Merge branch 'master-hansa-05072018' into 'master-patch-july-2018'

Master hansa 05072018

See merge request flectra-hq/flectra!100
This commit is contained in:
Parthiv Patel 2018-07-13 11:21:23 +00:00
commit e313b2a500
42 changed files with 958 additions and 88 deletions

View File

@ -9,7 +9,7 @@ class AccountRegisterPayments(models.TransientModel):
_inherit = "account.register.payments" _inherit = "account.register.payments"
check_amount_in_words = fields.Char(string="Amount in Words") check_amount_in_words = fields.Char(string="Amount in Words")
check_manual_sequencing = fields.Boolean(related='journal_id.check_manual_sequencing') check_manual_sequencing = fields.Boolean(related='journal_id.check_manual_sequencing', readonly=1)
# Note: a check_number == 0 means that it will be attributed when the check is printed # Note: a check_number == 0 means that it will be attributed when the check is printed
check_number = fields.Integer(string="Check Number", readonly=True, copy=False, default=0, check_number = fields.Integer(string="Check Number", readonly=True, copy=False, default=0,
help="Number of the check corresponding to this payment. If your pre-printed check are not already numbered, " help="Number of the check corresponding to this payment. If your pre-printed check are not already numbered, "
@ -42,7 +42,7 @@ class AccountPayment(models.Model):
_inherit = "account.payment" _inherit = "account.payment"
check_amount_in_words = fields.Char(string="Amount in Words") check_amount_in_words = fields.Char(string="Amount in Words")
check_manual_sequencing = fields.Boolean(related='journal_id.check_manual_sequencing') check_manual_sequencing = fields.Boolean(related='journal_id.check_manual_sequencing', readonly=1)
check_number = fields.Integer(string="Check Number", readonly=True, copy=False, check_number = fields.Integer(string="Check Number", readonly=True, copy=False,
help="The selected journal is configured to print check numbers. If your pre-printed check paper already has numbers " help="The selected journal is configured to print check numbers. If your pre-printed check paper already has numbers "
"or if the current numbering is wrong, you can change it in the journal configuration page.") "or if the current numbering is wrong, you can change it in the journal configuration page.")

View File

@ -5,7 +5,7 @@ import logging
from flectra import api, fields, models from flectra import api, fields, models
from flectra import tools, _ from flectra import tools, _
from flectra.exceptions import ValidationError from flectra.exceptions import ValidationError, AccessError
from flectra.modules.module import get_module_resource from flectra.modules.module import get_module_resource
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)

View File

@ -14,7 +14,7 @@ class MailAlias(models.AbstractModel):
_inherit = 'mail.alias.mixin' _inherit = 'mail.alias.mixin'
def _alias_check_contact_on_record(self, record, message, message_dict, alias): def _alias_check_contact_on_record(self, record, message, message_dict, alias):
if alias.alias_contact == 'employees' and record.ids: if alias.alias_contact == 'employees':
email_from = tools.decode_message_header(message, 'From') email_from = tools.decode_message_header(message, 'From')
email_address = tools.email_split(email_from)[0] email_address = tools.email_split(email_from)[0]
employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1) employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1)

View File

@ -38,11 +38,18 @@ var GreetingMessage = Widget.extend({
if (this.next_action != 'hr_attendance.hr_attendance_action_kiosk_mode' && this.next_action.tag != 'hr_attendance_kiosk_mode') { if (this.next_action != 'hr_attendance.hr_attendance_action_kiosk_mode' && this.next_action.tag != 'hr_attendance_kiosk_mode') {
this.activeBarcode = false; this.activeBarcode = false;
} }
this.attendance = action.attendance; this.attendance = action.attendance;
// We receive the check in/out times in UTC
// This widget only deals with display, which should be in browser's TimeZone
this.attendance.check_in = this.attendance.check_in && moment.utc(this.attendance.check_in).local();
this.attendance.check_out = this.attendance.check_out && moment.utc(this.attendance.check_out).local();
this.previous_attendance_change_date = action.previous_attendance_change_date && moment.utc(action.previous_attendance_change_date).local();
// check in/out times displayed in the greeting message template. // check in/out times displayed in the greeting message template.
this.attendance.check_in_time = (new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8); this.format_time = 'HH:mm:ss';
this.attendance.check_out_time = this.attendance.check_out && (new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8); this.attendance.check_in_time = this.attendance.check_in && this.attendance.check_in.format(this.format_time);
this.previous_attendance_change_date = action.previous_attendance_change_date; this.attendance.check_out_time = this.attendance.check_out && this.attendance.check_out.format(this.format_time);
this.employee_name = action.employee_name; this.employee_name = action.employee_name;
}, },
@ -57,13 +64,13 @@ var GreetingMessage = Widget.extend({
welcome_message: function() { welcome_message: function() {
var self = this; var self = this;
var now = new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000); var now = this.attendance.check_in.clone();
this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
if (now.getHours() < 5) { if (now.hours() < 5) {
this.$('.o_hr_attendance_message_message').append(_t("Good night")); this.$('.o_hr_attendance_message_message').append(_t("Good night"));
} else if (now.getHours() < 12) { } else if (now.hours() < 12) {
if (now.getHours() < 8 && Math.random() < 0.3) { if (now.hours() < 8 && Math.random() < 0.3) {
if (Math.random() < 0.75) { if (Math.random() < 0.75) {
this.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm")); this.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm"));
} else { } else {
@ -72,16 +79,16 @@ var GreetingMessage = Widget.extend({
} else { } else {
this.$('.o_hr_attendance_message_message').append(_t("Good morning")); this.$('.o_hr_attendance_message_message').append(_t("Good morning"));
} }
} else if (now.getHours() < 17){ } else if (now.hours() < 17){
this.$('.o_hr_attendance_message_message').append(_t("Good afternoon")); this.$('.o_hr_attendance_message_message').append(_t("Good afternoon"));
} else if (now.getHours() < 23){ } else if (now.hours() < 23){
this.$('.o_hr_attendance_message_message').append(_t("Good evening")); this.$('.o_hr_attendance_message_message').append(_t("Good evening"));
} else { } else {
this.$('.o_hr_attendance_message_message').append(_t("Good night")); this.$('.o_hr_attendance_message_message').append(_t("Good night"));
} }
if(this.previous_attendance_change_date){ if(this.previous_attendance_change_date){
var last_check_out_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000); var last_check_out_date = this.previous_attendance_change_date.clone();
if(now.valueOf() - last_check_out_date.valueOf() > 1000*60*60*24*7){ if(now - last_check_out_date > 24*7*60*60*1000){
this.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!")); this.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!"));
} else { } else {
if(Math.random() < 0.02){ if(Math.random() < 0.02){
@ -93,33 +100,33 @@ var GreetingMessage = Widget.extend({
farewell_message: function() { farewell_message: function() {
var self = this; var self = this;
var now = new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000); var now = this.attendance.check_out.clone();
this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
if(this.previous_attendance_change_date){ if(this.previous_attendance_change_date){
var last_check_in_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000); var last_check_in_date = this.previous_attendance_change_date.clone();
if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*12){ if(now - last_check_in_date > 1000*60*60*12){
this.$('.o_hr_attendance_warning_message').append(_t("Warning! Last check in was over 12 hours ago.<br/>If this isn't right, please contact Human Resources.")); this.$('.o_hr_attendance_warning_message').append(_t("Warning! Last check in was over 12 hours ago.<br/>If this isn't right, please contact Human Resources."));
clearTimeout(this.return_to_main_menu); clearTimeout(this.return_to_main_menu);
this.activeBarcode = false; this.activeBarcode = false;
} else if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*8){ } else if(now - last_check_in_date > 1000*60*60*8){
this.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!")); this.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!"));
} }
} }
if (now.getHours() < 12) { if (now.hours() < 12) {
this.$('.o_hr_attendance_message_message').append(_t("Have a good day!")); this.$('.o_hr_attendance_message_message').append(_t("Have a good day!"));
} else if (now.getHours() < 14) { } else if (now.hours() < 14) {
this.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!")); this.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!"));
if (Math.random() < 0.05) { if (Math.random() < 0.05) {
this.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar")); this.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar"));
} else if (Math.random() < 0.06) { } else if (Math.random() < 0.06) {
this.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away")); this.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away"));
} }
} else if (now.getHours() < 17) { } else if (now.hours() < 17) {
this.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon")); this.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon"));
} else { } else {
if (now.getHours() < 18 && Math.random() < 0.2) { if (now.hours() < 18 && Math.random() < 0.2) {
this.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise")); this.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise"));
} else { } else {
this.$('.o_hr_attendance_message_message').append(_t("Have a good evening")); this.$('.o_hr_attendance_message_message').append(_t("Have a good evening"));

View File

@ -1,6 +1,7 @@
flectra.define('hr_attendance.kiosk_mode', function (require) { flectra.define('hr_attendance.kiosk_mode', function (require) {
"use strict"; "use strict";
var ajax = require('web.ajax');
var core = require('web.core'); var core = require('web.core');
var Widget = require('web.Widget'); var Widget = require('web.Widget');
var Session = require('web.session'); var Session = require('web.session');
@ -27,6 +28,8 @@ var KioskMode = Widget.extend({
self.$el.html(QWeb.render("HrAttendanceKioskMode", {widget: self})); self.$el.html(QWeb.render("HrAttendanceKioskMode", {widget: self}));
self.start_clock(); self.start_clock();
}); });
// Make a RPC call every day to keep the session alive
self._interval = window.setInterval(this._callServer.bind(this), (60*60*1000*24));
return $.when(def, this._super.apply(this, arguments)); return $.when(def, this._super.apply(this, arguments));
}, },
@ -55,8 +58,15 @@ var KioskMode = Widget.extend({
destroy: function () { destroy: function () {
core.bus.off('barcode_scanned', this, this._onBarcodeScanned); core.bus.off('barcode_scanned', this, this._onBarcodeScanned);
clearInterval(this.clock_start); clearInterval(this.clock_start);
clearInterval(this._interval);
this._super.apply(this, arguments); this._super.apply(this, arguments);
}, },
_callServer: function () {
// Make a call to the database to avoid the auto close of the session
return ajax.rpc("/web/webclient/version_info", {});
},
}); });
core.action_registry.add('hr_attendance_kiosk_mode', KioskMode); core.action_registry.add('hr_attendance_kiosk_mode', KioskMode);

View File

@ -67,7 +67,7 @@ class Contract(models.Model):
resource_calendar_id = fields.Many2one( resource_calendar_id = fields.Many2one(
'resource.calendar', 'Working Schedule', 'resource.calendar', 'Working Schedule',
default=lambda self: self.env['res.company']._company_default_get().resource_calendar_id.id) default=lambda self: self.env['res.company']._company_default_get().resource_calendar_id.id)
wage = fields.Monetary('Wage', digits=(16, 2), required=True, help="Employee's monthly gross wage.") wage = fields.Monetary('Wage', digits=(16, 2), required=True, track_visibility="onchange", help="Employee's monthly gross wage.")
advantages = fields.Text('Advantages') advantages = fields.Text('Advantages')
notes = fields.Text('Notes') notes = fields.Text('Notes')
state = fields.Selection([ state = fields.Selection([

View File

@ -370,7 +370,9 @@ class HrExpense(models.Model):
product = default_product product = default_product
else: else:
expense_description = expense_description.replace(product_code.group(), '') expense_description = expense_description.replace(product_code.group(), '')
product = self.env['product.product'].search([('default_code', 'ilike', product_code.group(1))]) or default_product products = self.env['product.product'].search([('default_code', 'ilike', product_code.group(1))]) or default_product
product = products.filtered(lambda p: p.default_code == product_code.group(1)) or products[0]
account = product.product_tmpl_id._get_product_accounts()['expense']
pattern = '[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?' pattern = '[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?'
# Match the last occurence of a float in the string # Match the last occurence of a float in the string
@ -397,6 +399,8 @@ class HrExpense(models.Model):
'unit_amount': price, 'unit_amount': price,
'company_id': employee.company_id.id, 'company_id': employee.company_id.id,
}) })
if account:
custom_values['account_id'] = account.id
return super(HrExpense, self).message_new(msg_dict, custom_values) return super(HrExpense, self).message_new(msg_dict, custom_values)
class HrExpenseSheet(models.Model): class HrExpenseSheet(models.Model):
@ -512,7 +516,7 @@ class HrExpenseSheet(models.Model):
@api.onchange('employee_id') @api.onchange('employee_id')
def _onchange_employee_id(self): def _onchange_employee_id(self):
self.address_id = self.employee_id.address_home_id self.address_id = self.employee_id.sudo().address_home_id
self.department_id = self.employee_id.department_id self.department_id = self.employee_id.department_id
@api.one @api.one

View File

@ -82,6 +82,7 @@
<tbody> <tbody>
<tr> <tr>
<td style="padding-top:10px;font-size: 12px;"> <td style="padding-top:10px;font-size: 12px;">
<div>Sent by ${object.company_id.name}</div>
% if 'website_url' in object.job_id and object.job_id.website_url: % if 'website_url' in object.job_id and object.job_id.website_url:
<div> <div>
Discover <a href="/jobs" style="text-decoration:none;color:#717188;">our others jobs</a>. Discover <a href="/jobs" style="text-decoration:none;color:#717188;">our others jobs</a>.

View File

@ -36,7 +36,7 @@
<span t-field="l.user_id.partner_id.name"/> <span t-field="l.user_id.partner_id.name"/>
</td> </td>
<td > <td >
<span t-field="l.name"/> <span t-field="l.name" t-options="{'widget': 'text'}"/>
</td> </td>
<td t-if="show_task or show_project"> <td t-if="show_task or show_project">
<t t-if="show_project"><span t-field="l.project_id.name"/></t> <t t-if="show_project"><span t-field="l.project_id.name"/></t>

View File

@ -2,6 +2,7 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
import pytz
from flectra import api, exceptions, fields, models, _ from flectra import api, exceptions, fields, models, _
@ -77,7 +78,7 @@ class MailActivity(models.Model):
summary = fields.Char('Summary') summary = fields.Char('Summary')
note = fields.Html('Note') note = fields.Html('Note')
feedback = fields.Html('Feedback') feedback = fields.Html('Feedback')
date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.today) date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.context_today)
# description # description
user_id = fields.Many2one( user_id = fields.Many2one(
'res.users', 'Assigned to', 'res.users', 'Assigned to',
@ -108,8 +109,16 @@ class MailActivity(models.Model):
@api.depends('date_deadline') @api.depends('date_deadline')
def _compute_state(self): def _compute_state(self):
today = date.today() today_default = date.today()
for record in self.filtered(lambda activity: activity.date_deadline): for record in self.filtered(lambda activity: activity.date_deadline):
today = today_default
tz = record.user_id.sudo().tz
if tz:
today_utc = pytz.UTC.localize(datetime.utcnow())
today_tz = today_utc.astimezone(pytz.timezone(tz))
today = date(year=today_tz.year, month=today_tz.month, day=today_tz.day)
date_deadline = fields.Date.from_string(record.date_deadline) date_deadline = fields.Date.from_string(record.date_deadline)
diff = (date_deadline - today) diff = (date_deadline - today)
if diff.days == 0: if diff.days == 0:
@ -132,7 +141,8 @@ class MailActivity(models.Model):
@api.onchange('recommended_activity_type_id') @api.onchange('recommended_activity_type_id')
def _onchange_recommended_activity_type_id(self): def _onchange_recommended_activity_type_id(self):
self.activity_type_id = self.recommended_activity_type_id if self.recommended_activity_type_id:
self.activity_type_id = self.recommended_activity_type_id
@api.multi @api.multi
def _check_access(self, operation): def _check_access(self, operation):

View File

@ -505,7 +505,15 @@ class Channel(models.Model):
partners_to_add = partners - channel.channel_partner_ids partners_to_add = partners - channel.channel_partner_ids
channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]}) channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]})
for partner in partners_to_add: for partner in partners_to_add:
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,) if partner.id != self.env.user.partner_id.id:
notification = _('<div class="o_mail_notification">%(author)s invited %(new_partner)s to <a href="#" class="o_channel_redirect" data-oe-id="%(channel_id)s">#%(channel_name)s</a></div>') % {
'author': self.env.user.display_name,
'new_partner': partner.display_name,
'channel_id': channel.id,
'channel_name': channel.name,
}
else:
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (channel.id, channel.name,)
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id) self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id)
# broadcast the channel header to the added partner # broadcast the channel header to the added partner
@ -674,7 +682,7 @@ class Channel(models.Model):
msg += _(" This channel is private. People must be invited to join it.") msg += _(" This channel is private. People must be invited to join it.")
else: else:
channel_partners = self.env['mail.channel.partner'].search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)]) channel_partners = self.env['mail.channel.partner'].search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)])
msg = _("You are in a private conversation with <b>@%s</b>.") % channel_partners[0].partner_id.name msg = _("You are in a private conversation with <b>@%s</b>.") % (channel_partners[0].partner_id.name if channel_partners else _('Anonymous'))
msg += _("""<br><br> msg += _("""<br><br>
You can mention someone by typing <b>@username</b>, this will grab its attention.<br> You can mention someone by typing <b>@username</b>, this will grab its attention.<br>
You can mention a channel by typing <b>#channel</b>.<br> You can mention a channel by typing <b>#channel</b>.<br>

View File

@ -5,6 +5,7 @@ import logging
import re import re
from email.utils import formataddr from email.utils import formataddr
from openerp.http import request
from flectra import _, api, fields, models, modules, SUPERUSER_ID, tools from flectra import _, api, fields, models, modules, SUPERUSER_ID, tools
from flectra.exceptions import UserError, AccessError from flectra.exceptions import UserError, AccessError
@ -299,11 +300,12 @@ class Message(models.Model):
# 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'mimetype']) attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'mimetype'])
safari = request and request.httprequest.user_agent.browser == 'safari'
attachments_tree = dict((attachment['id'], { attachments_tree = dict((attachment['id'], {
'id': attachment['id'], 'id': attachment['id'],
'filename': attachment['datas_fname'], 'filename': attachment['datas_fname'],
'name': attachment['name'], 'name': attachment['name'],
'mimetype': attachment['mimetype'], 'mimetype': 'application/octet-stream' if safari and 'video' in attachment['mimetype'] else attachment['mimetype'],
}) for attachment in attachments_data) }) for attachment in attachments_data)
# 3. Tracking values # 3. Tracking values
@ -422,7 +424,18 @@ class Message(models.Model):
subtype_ids = [msg['subtype_id'][0] for msg in message_values if msg['subtype_id']] subtype_ids = [msg['subtype_id'][0] for msg in message_values if msg['subtype_id']]
subtypes = self.env['mail.message.subtype'].sudo().browse(subtype_ids).read(['internal', 'description']) subtypes = self.env['mail.message.subtype'].sudo().browse(subtype_ids).read(['internal', 'description'])
subtypes_dict = dict((subtype['id'], subtype) for subtype in subtypes) subtypes_dict = dict((subtype['id'], subtype) for subtype in subtypes)
# fetch notification status
notif_dict = {}
notifs = self.env['mail.notification'].sudo().search([('mail_message_id', 'in', list(mid for mid in message_tree)), ('is_read', '=', False)])
for notif in notifs:
mid = notif.mail_message_id.id
if not notif_dict.get(mid):
notif_dict[mid] = {'partner_id': list()}
notif_dict[mid]['partner_id'].append(notif.res_partner_id.id)
for message in message_values: for message in message_values:
message['needaction_partner_ids'] = notif_dict.get(message['id'], dict()).get('partner_id', [])
message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['internal'] message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['internal']
message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description'] message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description']
if message['model'] and self.env[message['model']]._original_module: if message['model'] and self.env[message['model']]._original_module:

View File

@ -126,7 +126,8 @@ class MailThread(models.AbstractModel):
followers = self.env['mail.followers'].sudo().search([ followers = self.env['mail.followers'].sudo().search([
('res_model', '=', self._name), ('res_model', '=', self._name),
('partner_id', operator, operand)]) ('partner_id', operator, operand)])
return [('id', 'in', followers.mapped('res_id'))] # using read() below is much faster than followers.mapped('res_id')
return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
@api.model @api.model
def _search_follower_channels(self, operator, operand): def _search_follower_channels(self, operator, operand):
@ -139,7 +140,8 @@ class MailThread(models.AbstractModel):
followers = self.env['mail.followers'].sudo().search([ followers = self.env['mail.followers'].sudo().search([
('res_model', '=', self._name), ('res_model', '=', self._name),
('channel_id', operator, operand)]) ('channel_id', operator, operand)])
return [('id', 'in', followers.mapped('res_id'))] # using read() below is much faster than followers.mapped('res_id')
return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
@api.multi @api.multi
@api.depends('message_follower_ids') @api.depends('message_follower_ids')
@ -149,7 +151,8 @@ class MailThread(models.AbstractModel):
('res_id', 'in', self.ids), ('res_id', 'in', self.ids),
('partner_id', '=', self.env.user.partner_id.id), ('partner_id', '=', self.env.user.partner_id.id),
]) ])
following_ids = followers.mapped('res_id') # using read() below is much faster than followers.mapped('res_id')
following_ids = [res['res_id'] for res in followers.read(['res_id'])]
for record in self: for record in self:
record.message_is_follower = record.id in following_ids record.message_is_follower = record.id in following_ids
@ -161,9 +164,11 @@ class MailThread(models.AbstractModel):
]) ])
# Cases ('message_is_follower', '=', True) or ('message_is_follower', '!=', False) # Cases ('message_is_follower', '=', True) or ('message_is_follower', '!=', False)
if (operator == '=' and operand) or (operator == '!=' and not operand): if (operator == '=' and operand) or (operator == '!=' and not operand):
return [('id', 'in', followers.mapped('res_id'))] # using read() below is much faster than followers.mapped('res_id')
return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
else: else:
return [('id', 'not in', followers.mapped('res_id'))] # using read() below is much faster than followers.mapped('res_id')
return [('id', 'not in', [res['res_id'] for res in followers.read(['res_id'])])]
@api.multi @api.multi
def _get_message_unread(self): def _get_message_unread(self):
@ -1373,7 +1378,13 @@ class MailThread(models.AbstractModel):
located in tools. """ located in tools. """
if not body: if not body:
return body, attachments return body, attachments
root = lxml.html.fromstring(body) try:
root = lxml.html.fromstring(body)
except ValueError:
# In case the email client sent XHTML, fromstring will fail because 'Unicode strings
# with encoding declaration are not supported'.
root = lxml.html.fromstring(body.encode('utf-8'))
postprocessed = False postprocessed = False
to_remove = [] to_remove = []
for node in root.iter(): for node in root.iter():

View File

@ -58,6 +58,8 @@ class Users(models.Model):
# create a welcome message # create a welcome message
user._create_welcome_message() user._create_welcome_message()
# Auto-subscribe to channels
self.env['mail.channel'].search([('group_ids', 'in', user.groups_id.ids)])._subscribe_users()
return user return user
@api.multi @api.multi
@ -127,16 +129,19 @@ class Users(models.Model):
def activity_user_count(self): def activity_user_count(self):
query = """SELECT m.id, count(*), act.res_model as model, query = """SELECT m.id, count(*), act.res_model as model,
CASE CASE
WHEN now()::date - act.date_deadline::date = 0 Then 'today' WHEN %(today)s::date - act.date_deadline::date = 0 Then 'today'
WHEN now()::date - act.date_deadline::date > 0 Then 'overdue' WHEN %(today)s::date - act.date_deadline::date > 0 Then 'overdue'
WHEN now()::date - act.date_deadline::date < 0 Then 'planned' WHEN %(today)s::date - act.date_deadline::date < 0 Then 'planned'
END AS states END AS states
FROM mail_activity AS act FROM mail_activity AS act
JOIN ir_model AS m ON act.res_model_id = m.id JOIN ir_model AS m ON act.res_model_id = m.id
WHERE user_id = %s WHERE user_id = %(user_id)s
GROUP BY m.id, states, act.res_model; GROUP BY m.id, states, act.res_model;
""" """
self.env.cr.execute(query, [self.env.uid]) self.env.cr.execute(query, {
'today': fields.Date.context_today(self),
'user_id': self.env.uid,
})
activity_data = self.env.cr.dictfetchall() activity_data = self.env.cr.dictfetchall()
model_ids = [a['id'] for a in activity_data] model_ids = [a['id'] for a in activity_data]
model_names = {n[0]:n[1] for n in self.env['ir.model'].browse(model_ids).name_get()} model_names = {n[0]:n[1] for n in self.env['ir.model'].browse(model_ids).name_get()}

View File

@ -32,6 +32,7 @@ function _readActivities(self, ids) {
model: 'mail.activity', model: 'mail.activity',
method: 'read', method: 'read',
args: [ids], args: [ids],
context: (self.record && self.record.getContext()) || self.getSession().user_context,
}).then(function (activities) { }).then(function (activities) {
// convert create_date and date_deadline to moments // convert create_date and date_deadline to moments
_.each(activities, function (activity) { _.each(activities, function (activity) {
@ -108,6 +109,7 @@ var AbstractActivityField = AbstractField.extend({
method: 'action_feedback', method: 'action_feedback',
args: [[id]], args: [[id]],
kwargs: {feedback: feedback}, kwargs: {feedback: feedback},
context: this.record.getContext(),
}); });
}, },
_scheduleActivity: function (id, previous_activity_type_id, callback) { _scheduleActivity: function (id, previous_activity_type_id, callback) {

View File

@ -149,7 +149,16 @@ function make_message (data) {
_.each(_.keys(emoji_substitutions), function (key) { _.each(_.keys(emoji_substitutions), function (key) {
var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1'); var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?=\\s|$|</[a-z]*>)", "g"); var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?=\\s|$|</[a-z]*>)", "g");
var msg_bak = msg.body;
msg.body = msg.body.replace(regexp, ' <span class="o_mail_emoji">'+emoji_substitutions[key]+'</span> '); msg.body = msg.body.replace(regexp, ' <span class="o_mail_emoji">'+emoji_substitutions[key]+'</span> ');
// Idiot-proof limit. If the user had the amazing idea of copy-pasting thousands of emojis,
// the image rendering can lead to memory overflow errors on some browsers (e.g. Chrome).
// Set an arbitrary limit to 200 from which we simply don't replace them (anyway, they are
// already replaced by the unicode counterpart).
if (_.str.count(msg.body, 'o_mail_emoji') > 200) {
msg.body = msg_bak;
}
}); });
function property_descr(channel) { function property_descr(channel) {

View File

@ -16,7 +16,8 @@ var HEIGHT_FOLDED = '34px';
return Widget.extend({ return Widget.extend({
template: "mail.ChatWindow", template: "mail.ChatWindow",
custom_events: { custom_events: {
escape_pressed: '_onEscapePressed' escape_pressed: '_onEscapePressed',
document_viewer_closed: '_onDocumentViewerClose',
}, },
events: { events: {
'click .o_chat_composer': '_onComposerClick', 'click .o_chat_composer': '_onComposerClick',
@ -180,6 +181,9 @@ return Widget.extend({
} }
this.focus_input(); this.focus_input();
}, },
_onDocumentViewerClose: function (ev) {
this.focus_input();
},
/** /**
* @private * @private
*/ */

View File

@ -514,6 +514,9 @@ var BasicComposer = Widget.extend(chat_mixin, {
on_click_add_attachment: function () { on_click_add_attachment: function () {
this.$('input.o_input_file').click(); this.$('input.o_input_file').click();
this.$input.focus(); this.$input.focus();
// set ignoreEscape to avoid escape_pressed event when file selector dialog is opened
// when user press escape to cancel file selector dialog then escape_pressed event should not be trigerred
this.ignoreEscape = true;
}, },
setState: function (state) { setState: function (state) {
@ -565,6 +568,8 @@ var BasicComposer = Widget.extend(chat_mixin, {
if (this.mention_manager.is_open()) { if (this.mention_manager.is_open()) {
event.stopPropagation(); event.stopPropagation();
this.mention_manager.reset_suggestions(); this.mention_manager.reset_suggestions();
} else if (this.ignoreEscape) {
this.ignoreEscape = false;
} else { } else {
this.trigger_up("escape_pressed"); this.trigger_up("escape_pressed");
} }
@ -789,6 +794,7 @@ var BasicComposer = Widget.extend(chat_mixin, {
* @param {MouseEvent} event * @param {MouseEvent} event
*/ */
_onAttachmentView: function (event) { _onAttachmentView: function (event) {
event.stopPropagation();
var activeAttachmentID = $(event.currentTarget).data('id'); var activeAttachmentID = $(event.currentTarget).data('id');
var attachments = this.get('attachment_ids'); var attachments = this.get('attachment_ids');
if (activeAttachmentID) { if (activeAttachmentID) {

View File

@ -25,6 +25,7 @@ var DocumentViewer = Widget.extend({
'DOMMouseScroll .o_viewer_content': '_onScroll', // Firefox 'DOMMouseScroll .o_viewer_content': '_onScroll', // Firefox
'mousewheel .o_viewer_content': '_onScroll', // Chrome, Safari, IE 'mousewheel .o_viewer_content': '_onScroll', // Chrome, Safari, IE
'keydown': '_onKeydown', 'keydown': '_onKeydown',
'keyup': '_onKeyUp',
'mousedown .o_viewer_img': '_onStartDrag', 'mousedown .o_viewer_img': '_onStartDrag',
'mousemove .o_viewer_content': '_onDrag', 'mousemove .o_viewer_content': '_onDrag',
'mouseup .o_viewer_content': '_onEndDrag' 'mouseup .o_viewer_content': '_onEndDrag'
@ -152,6 +153,7 @@ var DocumentViewer = Widget.extend({
_onClose: function (e) { _onClose: function (e) {
e.preventDefault(); e.preventDefault();
this.$el.modal('hide'); this.$el.modal('hide');
this.trigger_up('document_viewer_closed');
}, },
/** /**
* When popup close complete destroyed modal even DOM footprint too * When popup close complete destroyed modal even DOM footprint too
@ -232,6 +234,20 @@ var DocumentViewer = Widget.extend({
break; break;
} }
}, },
/**
* Close popup on ESCAPE keyup
*
* @private
* @param {KeyEvent} e
*/
_onKeyUp: function (e) {
switch (e.which) {
case $.ui.keyCode.ESCAPE:
e.preventDefault();
this._onClose(e);
break;
}
},
/** /**
* @private * @private
* @param {MouseEvent} e * @param {MouseEvent} e

View File

@ -122,7 +122,7 @@ var MessagingMenu = Widget.extend({
if (channelID === 'channel_inbox') { if (channelID === 'channel_inbox') {
var resID = $(event.currentTarget).data('res_id'); var resID = $(event.currentTarget).data('res_id');
var resModel = $(event.currentTarget).data('res_model'); var resModel = $(event.currentTarget).data('res_model');
if (resModel && resID) { if (resModel && resModel !== 'mail.channel' && resID) {
this.do_action({ this.do_action({
type: 'ir.actions.act_window', type: 'ir.actions.act_window',
res_model: resModel, res_model: resModel,
@ -130,7 +130,11 @@ var MessagingMenu = Widget.extend({
res_id: resID res_id: resID
}); });
} else { } else {
this.do_action('mail.mail_channel_action_client_chat', {clear_breadcrumbs: true}) var clientChatOptions = {clear_breadcrumbs: true};
if (resModel && resModel === 'mail.channel' && resID) {
clientChatOptions.active_id = resID;
}
this.do_action('mail.mail_channel_action_client_chat', clientChatOptions)
.then(function () { .then(function () {
self.trigger_up('hide_app_switcher'); self.trigger_up('hide_app_switcher');
core.bus.trigger('change_menu_section', chat_manager.get_discuss_menu_id()); core.bus.trigger('change_menu_section', chat_manager.get_discuss_menu_id());

View File

@ -328,6 +328,7 @@ var Thread = Widget.extend({
* @param {MouseEvent} event * @param {MouseEvent} event
*/ */
_onAttachmentView: function (event) { _onAttachmentView: function (event) {
event.stopPropagation();
var activeAttachmentID = $(event.currentTarget).data('id'); var activeAttachmentID = $(event.currentTarget).data('id');
if (activeAttachmentID) { if (activeAttachmentID) {
var attachmentViewer = new DocumentViewer(this, this.attachments, activeAttachmentID); var attachmentViewer = new DocumentViewer(this, this.attachments, activeAttachmentID);

View File

@ -60,7 +60,7 @@ function _parse_and_transform(nodes, transform_function) {
// Suggested URL Javascript regex of http://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url // Suggested URL Javascript regex of http://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
// Adapted to make http(s):// not required if (and only if) www. is given. So `should.notmatch` does not match. // Adapted to make http(s):// not required if (and only if) www. is given. So `should.notmatch` does not match.
var url_regexp = /\b(?:https?:\/\/|(www\.))[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,13}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/gi; var url_regexp = /\b(?:https?:\/\/\d{1,3}(?:\.\d{1,3}){3}|(?:https?:\/\/|(?:www\.))[-a-z0-9@:%._\+~#=]{2,256}\.[a-z]{2,13})\b(?:[-a-z0-9@:%_\+.~#?&'$//=]*)/gi;
function linkify(text, attrs) { function linkify(text, attrs) {
attrs = attrs || {}; attrs = attrs || {};
if (attrs.target === undefined) { if (attrs.target === undefined) {
@ -70,7 +70,7 @@ function linkify(text, attrs) {
return key + '="' + _.escape(value) + '"'; return key + '="' + _.escape(value) + '"';
}).join(' '); }).join(' ');
return text.replace(url_regexp, function (url) { return text.replace(url_regexp, function (url) {
var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url; var href = (!/^https?:\/\//i.test(url)) ? "http://" + url : url;
return '<a ' + attrs + ' href="' + href + '">' + url + '</a>'; return '<a ' + attrs + ' href="' + href + '">' + url + '</a>';
}); });
} }
@ -92,6 +92,7 @@ function strip_html (node, transform_children) {
function inline (node, transform_children) { function inline (node, transform_children) {
if (node.nodeType === 3) return node.data; if (node.nodeType === 3) return node.data;
if (node.nodeType === 8) return "";
if (node.tagName === "BR") return " "; if (node.tagName === "BR") return " ";
if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return transform_children(); if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return transform_children();
node.innerHTML = transform_children(); node.innerHTML = transform_children();
@ -100,16 +101,23 @@ function inline (node, transform_children) {
// Parses text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False // Parses text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False
function parse_email (text) { function parse_email (text) {
var result = text.match(/(.*)<(.*@.*)>/); if (text){
if (result) { var result = text.match(/(.*)<(.*@.*)>/);
return [_.str.trim(result[1]), _.str.trim(result[2])]; if (result) {
return [_.str.trim(result[1]), _.str.trim(result[2])];
}
result = text.match(/(.*@.*)/);
if (result) {
return [_.str.trim(result[1]), _.str.trim(result[1])];
}
return [text, false];
} }
result = text.match(/(.*@.*)/); /* result = text.match(/(.*@.*)/);
if (result) { if (result) {
return [_.str.trim(result[1]), _.str.trim(result[1])]; return [_.str.trim(result[1]), _.str.trim(result[1])];
} }
return [text, false]; return [text, false];
} }*/
// Replaces textarea text into html text (add <p>, <a>) // Replaces textarea text into html text (add <p>, <a>)
// TDE note : should be done server-side, in Python -> use mail.compose.message ? // TDE note : should be done server-side, in Python -> use mail.compose.message ?

View File

@ -51,7 +51,7 @@
border: none; border: none;
} }
.o_composer_button_full_composer { .o_composer_button_full_composer {
.o-position-absolute(0, 0); .o-position-absolute(auto, 0);
} }
@media (max-width: @screen-xs-max) { @media (max-width: @screen-xs-max) {
.o_composer_button_send { .o_composer_button_send {
@ -136,7 +136,7 @@
&.o_chat_inline_composer { &.o_chat_inline_composer {
.o_composer_container { .o_composer_container {
.o-flex(1, 0, auto); .o-flex(1, 1, auto);
} }
.o_composer { .o_composer {
padding: @o-mail-chatter-gap @o-mail-chatter-gap 0 @o-mail-chatter-gap; padding: @o-mail-chatter-gap @o-mail-chatter-gap 0 @o-mail-chatter-gap;
@ -339,6 +339,7 @@
} }
.o_modal_fullscreen { .o_modal_fullscreen {
z-index: @o-chat-window-zindex + 1; // To overlap chat window
.o_viewer_content { .o_viewer_content {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -107,7 +107,7 @@
&.o_mail_note { &.o_mail_note {
background-color: @mail-thread-note; background-color: @mail-thread-note;
padding-left: @grid-gutter-width*0.3; padding-left: @grid-gutter-width*0.3;
border-bottom: 1px solid @gray-lighter-dark; border-bottom: 1px solid @gray-lighter-darker;
} }
.o_mail_subject { .o_mail_subject {

View File

@ -76,7 +76,7 @@
</t> </t>
<t t-name="DocumentViewer"> <t t-name="DocumentViewer">
<div class="modal o_modal_fullscreen" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal o_modal_fullscreen" tabindex="-1" data-keyboard="false" role="dialog" aria-hidden="true">
<t t-call="DocumentViewer.Content"/> <t t-call="DocumentViewer.Content"/>
<t t-if="widget.attachment.length != 1"> <t t-if="widget.attachment.length != 1">

View File

@ -50,6 +50,84 @@ QUnit.module('mail', {}, function () {
parent.destroy(); parent.destroy();
}); });
QUnit.test('open document viewer and close using ESCAPE key should reset focus to chat window', function (assert) {
assert.expect(6);
function createParent(params) {
var widget = new Widget();
testUtils.addMockEnvironment(widget, params);
return widget;
}
var messages = [{
attachment_ids: [{
filename: 'image1.jpg',
id:1,
mimetype: 'image/jpeg',
name: 'Test Image 1',
url: '/web/content/1?download=true'
}],
author_id: ["1", "John Doe"],
body: "A message",
date: moment("2016-12-20 09:35:40"),
displayed_author: "John Doe",
id: 1,
is_note: false,
is_starred: false,
model: 'partner',
res_id: 2
}];
var parent = createParent({
mockRPC: function (route, args) {
if(_.str.contains(route, '/mail/attachment/preview/') ||
_.str.contains(route, '/web/static/lib/pdfjs/web/viewer.html')){
var canvas = document.createElement('canvas');
return $.when(canvas.toDataURL());
}
return this._super.apply(this, arguments);
},
data: {},
});
var chatWindow = new ChatWindow(parent, 1, "user", false, messages.length, {});
chatWindow.appendTo($('#qunit-fixture'));
chatWindow.render(messages);
testUtils.intercept(chatWindow, 'get_messages', function(event) {
event.stopPropagation();
var requested_msgs = _.filter(messages, function (msg) {
return _.contains(event.data.options.ids, msg.id);
});
event.data.callback($.when(requested_msgs));
}, true);
testUtils.intercept(chatWindow, 'get_bus', function(event) {
event.stopPropagation();
event.data.callback(new Bus());
}, true);
chatWindow.on('document_viewer_closed', null, function () {
assert.ok(true, "chat window should trigger a close document viewer event");
});
assert.strictEqual(chatWindow.$('.o_thread_message .o_attachment').length, 1,
"there should be three attachment on message");
// click on first image attachement
chatWindow.$('.o_thread_message .o_attachment .o_image_box .o_image_overlay').first().click();
// check focus is on document viewer popup and then press escape to close it
assert.strictEqual(document.activeElement, $('.o_modal_fullscreen')[0], "Modal popup should have focus");
assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[src*="/web/image/1?unique=1"]').length, 1,
"Modal popup should open with first image src");
// trigger ESCAPE keyup on document viewer popup
var upKeyEvent = jQuery.Event("keyup", {which: 27});
$('.o_modal_fullscreen').trigger(upKeyEvent);
assert.strictEqual(document.activeElement, chatWindow.$input[0],
"input should be focused");
var upKeyEvent = jQuery.Event( "keyup", {which: 27});
chatWindow.$('.o_composer_input').trigger(upKeyEvent);
assert.strictEqual(chatWindow.folded, false, "Closed chat Window");
parent.destroy();
});
QUnit.test('chat window\'s input can still be focused when the UI is blocked', function (assert) { QUnit.test('chat window\'s input can still be focused when the UI is blocked', function (assert) {
assert.expect(2); assert.expect(2);

View File

@ -8,7 +8,7 @@ QUnit.module('mail', {}, function () {
QUnit.module('Mail utils'); QUnit.module('Mail utils');
QUnit.test('add_link utility function', function (assert) { QUnit.test('add_link utility function', function (assert) {
assert.expect(7); assert.expect(15);
var testInputs = { var testInputs = {
'http://admin:password@example.com:8/%2020': true, 'http://admin:password@example.com:8/%2020': true,
@ -24,6 +24,7 @@ QUnit.test('add_link utility function', function (assert) {
var output = utils.parse_and_transform(content, utils.add_link); var output = utils.parse_and_transform(content, utils.add_link);
if (willLinkify) { if (willLinkify) {
assert.strictEqual(output.indexOf('<a '), 0, "There should be a link"); assert.strictEqual(output.indexOf('<a '), 0, "There should be a link");
assert.strictEqual(output.indexOf('</a>'), (output.length - 4), "Link should match the whole text");
} else { } else {
assert.strictEqual(output.indexOf('<a '), -1, "There should be no link"); assert.strictEqual(output.indexOf('<a '), -1, "There should be no link");
} }

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import socket import socket
@ -10,7 +10,7 @@ from flectra.tools import mute_logger
MAIL_TEMPLATE = """Return-Path: <whatever-2a840@postmaster.twitter.com> MAIL_TEMPLATE = """Return-Path: <whatever-2a840@postmaster.twitter.com>
To: {to} To: {to}
cc: {cc} cc: {cc}
Received: by mail1.flectra.com (Postfix, from userid 10002) Received: by mail1.openerp.com (Postfix, from userid 10002)
id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST) id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
From: {email_from} From: {email_from}
Subject: {subject} Subject: {subject}
@ -51,7 +51,7 @@ Content-Transfer-Encoding: quoted-printable
MAIL_TEMPLATE_PLAINTEXT = """Return-Path: <whatever-2a840@postmaster.twitter.com> MAIL_TEMPLATE_PLAINTEXT = """Return-Path: <whatever-2a840@postmaster.twitter.com>
To: {to} To: {to}
Received: by mail1.flectra.com (Postfix, from userid 10002) Received: by mail1.openerp.com (Postfix, from userid 10002)
id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST) id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
From: Sylvie Lelitre <test.sylvie.lelitre@agrolait.com> From: Sylvie Lelitre <test.sylvie.lelitre@agrolait.com>
Subject: {subject} Subject: {subject}
@ -78,7 +78,7 @@ X-Spam-Status: No, score=-2.6 required=5.0 tests=BAYES_00,FREEMAIL_FROM,
HTML_MESSAGE,RCVD_IN_DNSWL_LOW autolearn=unavailable version=3.3.1 HTML_MESSAGE,RCVD_IN_DNSWL_LOW autolearn=unavailable version=3.3.1
Received: from mail-ie0-f173.google.com (mail-ie0-f173.google.com [209.85.223.173]) Received: from mail-ie0-f173.google.com (mail-ie0-f173.google.com [209.85.223.173])
by mail1.grosbedon.com (Postfix) with ESMTPS id 9BBD7BFAAA by mail1.grosbedon.com (Postfix) with ESMTPS id 9BBD7BFAAA
for <raoul@flectra.fr>; Fri, 23 Aug 2013 13:17:55 +0200 (CEST) for <raoul@openerp.fr>; Fri, 23 Aug 2013 13:17:55 +0200 (CEST)
Received: by mail-ie0-f173.google.com with SMTP id qd12so575130ieb.4 Received: by mail-ie0-f173.google.com with SMTP id qd12so575130ieb.4
for <raoul@grosbedon.fr>; Fri, 23 Aug 2013 04:17:54 -0700 (PDT) for <raoul@grosbedon.fr>; Fri, 23 Aug 2013 04:17:54 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
@ -298,6 +298,630 @@ AAAAACwAAAAAAgACAAAEA3DJFQA7
--001a11416b9e9b229a05272b7052-- --001a11416b9e9b229a05272b7052--
""" """
MAIL_EML_ATTACHMENT = """Subject: Re: test attac
From: Anon <anon@flectrahq.com>
To: anon@gmail.com
References: <f3b9f8f8-28fa-2543-cab2-7aa68f679ebb@flectrahq.com>
Message-ID: <cb7eaf62-58dc-2017-148c-305d0c78892f@flectrahq.com>
Date: Wed, 14 Mar 2018 14:26:58 +0100
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101
Thunderbird/52.6.0
MIME-Version: 1.0
In-Reply-To: <f3b9f8f8-28fa-2543-cab2-7aa68f679ebb@flectrahq.com>
Content-Type: multipart/mixed;
boundary="------------A6B5FD5F68F4D73ECD739009"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------A6B5FD5F68F4D73ECD739009
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 7bit
On 14/03/18 14:20, Anon wrote:
> Some nice content
>
--------------A6B5FD5F68F4D73ECD739009
Content-Type: message/rfc822;
name="original_msg.eml"
Content-Transfer-Encoding: 8bit
Content-Disposition: attachment;
filename="original_msg.eml"
Delivered-To: anon2@gmail1.openerp.com
Received: by 10.46.1.170 with SMTP id f42csp2379722lji;
Mon, 5 Mar 2018 01:19:23 -0800 (PST)
X-Google-Smtp-Source: AG47ELsYTlAcblMxfnaEENQuF+MFoac5Q07wieyw0cybq/qOX4+DmayqoQILkiWT+NiTOcnr/ACO
X-Received: by 10.28.154.213 with SMTP id c204mr7237750wme.64.1520241563503;
Mon, 05 Mar 2018 01:19:23 -0800 (PST)
ARC-Seal: i=1; a=rsa-sha256; t=1520241563; cv=none;
d=google.com; s=arc-20160816;
b=BqgMSbqmbpYW1ZtfGTVjj/654MBmabw4XadNZEaI96hDaub6N6cP8Guu3PoxscI9os
0OLYVP1s/B+Vv9rIzulCwHyHsgnX+aTxGYepTDN6x8SA9Qeb9aQoNSVvQLryTAoGpaFr
vXhw8aPWyr28edE03TDFA/s7X65Bf6dV5zJdMiUPVqGkfYfcTHMf3nDER5vk8vQj7tve
Cfyy0h9vLU9RSEtdFwmlEkLmgT9NQ3GDf0jQ97eMXPgR2q6duCPoMcz15KlWOno53xgH
EiV7aIZ5ZMN/m+/2xt3br/ubJ5euFojWhDnHUZoaqd08TCSQPd4fFCCx75MjDeCnwYMn
iKSg==
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
h=content-language:mime-version:user-agent:date:message-id:subject
:from:to:dkim-signature:arc-authentication-results;
bh=/UIFqhjCCbwBLsI4w7YY98QH6G/wxe+2W4bbMDCskjM=;
b=Wv5jt+usnSgWI96GaZWUN8/VKl1drueDpU/4gkyX/iK4d6S4CuSDjwYAc3guz/TjeW
GoKCqT30IGZoStpXQbuLry7ezXNK+Fp8MJKN2n/x5ClJWHxIsxIGlP2QC3TO8RI0P5o0
GXG9izW93q1ubkdPJFt3unSjjwSYf5XVQAZQtRm9xKjqA+lbtFbsnbjJ4wgYBURnD8ma
Qxb2xsxXDelaZvtdlzHRDn5SEkbqhcCclEYw6oRLpVQFZeYtPxcCleVybtj2owJxdaLp
7wXuo/gpYe6E2cPuS2opei8AzjEhYTNzlYXTPvaoxCCTTjfGTaPv22TeRDehuIXngSEl
Nmmw==
ARC-Authentication-Results: i=1; mx.google.com;
dkim=pass header.i=@flectrahq.com header.s=mail header.b=MCzhjB9b;
spf=pass (google.com: domain of soup@flectrahq.com designates 149.202.180.44 as permitted sender) smtp.mailfrom=soup@flectrahq.com;
dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=flectrahq.com
Return-Path: <soup@flectrahq.com>
Received: from mail2.flectrahq.com (mail2.flectrahq.com. [149.202.180.44])
by mx.google.com with ESMTPS id y4si4279200wmy.148.2018.03.05.01.19.22
(version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
Mon, 05 Mar 2018 01:19:23 -0800 (PST)
Received-SPF: pass (google.com: domain of soup@flectrahq.com designates 149.202.180.44 as permitted sender) client-ip=149.202.180.44;
Authentication-Results: mx.google.com;
dkim=pass header.i=@flectrahq.com header.s=mail header.b=MCzhjB9b;
spf=pass (google.com: domain of soup@flectrahq.com designates 149.202.180.44 as permitted sender) smtp.mailfrom=soup@flectrahq.com;
dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=flectrahq.com
Received: from [10.10.31.24] (unknown [91.183.114.50])
(Authenticated sender: soup)
by mail2.flectrahq.com (Postfix) with ESMTPSA id 7B571A4085
for <what@flectrahq.com>; Mon, 5 Mar 2018 10:19:21 +0100 (CET)
DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=flectrahq.com; s=mail;
t=1520241562; bh=L2r7Sp/vjogIdM1k8H9zDGDjnhKolsTTLLjndnFC4Jc=;
h=To:From:Subject:Date:From;
b=MCzhjB9bnsrJ3uKjq+GjujFxmtrq3fc7Vv7Vg2C72EPKnkxgqy6yPjWKtXbBlaiT3
YjKI24aiSQlOeOPQiqFgiDzeqqemNDp+CRuhoYz1Vbz+ESRaHtkWRLb7ZjvohS2k7e
RTq7tUxY2nUL2YrNHV7DFYtJVBwiTuyLP6eAiJdE=
To: what@flectrahq.com
From: Soup <soup@flectrahq.com>
Subject: =?UTF-8?Q?Soupe_du_jour_:_Pois_cass=c3=a9s?=
Message-ID: <a05d8334-7b7c-df68-c96a-4a88ed19f31b@flectrahq.com>
Date: Mon, 5 Mar 2018 10:19:21 +0100
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101
Thunderbird/52.6.0
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="------------1F2D18B1129FC2F0B9EECF50"
Content-Language: en-US
X-Spam-Status: No, score=-1.2 required=5.0 tests=ALL_TRUSTED,BAYES_00,
HTML_IMAGE_ONLY_08,HTML_MESSAGE,T_REMOTE_IMAGE autolearn=no
autolearn_force=no version=3.4.0
X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on mail2.flectrahq.com
This is a multi-part message in MIME format.
--------------1F2D18B1129FC2F0B9EECF50
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 8bit
Résultat de recherche d'images pour "dessin la princesse au petit pois"
--
Soup
Odoo S.A.
Chaussée de Namur, 40
B-1367 Grand Rosière
Web: http://www.flectrahq.com
--------------1F2D18B1129FC2F0B9EECF50
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 8bit
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
</head>
<body text="#000000" bgcolor="#FFFFFF">
<p><img
src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQjCNAadd3NDM8g9w0P_-gAVYrrqC0wmBNYKGsTZ2Pst5SsNxTRnA"
alt="Résultat de recherche d'images pour &quot;dessin la
princesse au petit pois&quot;"></p>
<pre class="moz-signature" cols="72">--
Soup
Odoo S.A.
Chaussée de Namur, 40
B-1367 Grand Rosière
Web: <a class="moz-txt-link-freetext" href="http://www.flectrahq.com">http://www.flectrahq.com</a> </pre>
</body>
</html>
--------------1F2D18B1129FC2F0B9EECF50--
--------------A6B5FD5F68F4D73ECD739009--"""
MAIL_XHTML = """Return-Path: <xxxx@xxxx.com>
Received: from xxxx.internal (xxxx.xxxx.internal [1.1.1.1])
by xxxx (xxxx 1.1.1-111-g972eecc-slipenbois) with LMTPA;
Fri, 13 Apr 2018 22:11:52 -0400
X-Cyrus-Session-Id: sloti35d1t38-1111111-11111111111-5-11111111111111111111
X-Sieve: CMU Sieve 1.0
X-Spam-known-sender: no ("Email failed DMARC policy for domain"); in-addressbook
X-Spam-score: 0.0
X-Spam-hits: ALL_TRUSTED -1, BAYES_00 -1.9, FREEMAIL_FROM 0.001,
HTML_FONT_LOW_CONTRAST 0.001, HTML_MESSAGE 0.001, SPF_SOFTFAIL 0.665,
LANGUAGES en, BAYES_USED global, SA_VERSION 1.1.0
X-Spam-source: IP='1.1.1.1', Host='unk', Country='unk', FromHeader='com',
MailFrom='com'
X-Spam-charsets: plain='utf-8', html='utf-8'
X-IgnoreVacation: yes ("Email failed DMARC policy for domain")
X-Resolved-to: catchall@xxxx.xxxx
X-Delivered-to: catchall@xxxx.xxxx
X-Mail-from: xxxx@xxxx.com
Received: from mx4 ([1.1.1.1])
by xxxx.internal (LMTPProxy); Fri, 13 Apr 2018 22:11:52 -0400
Received: from xxxx.xxxx.com (localhost [127.0.0.1])
by xxxx.xxxx.internal (Postfix) with ESMTP id E1111C1111;
Fri, 13 Apr 2018 22:11:51 -0400 (EDT)
Received: from xxxx.xxxx.internal (localhost [127.0.0.1])
by xxxx.xxxx.com (Authentication Milter) with ESMTP
id BBDD1111D1A;
Fri, 13 Apr 2018 22:11:51 -0400
ARC-Authentication-Results: i=1; xxxx.xxxx.com; arc=none (no signatures found);
dkim=pass (2048-bit rsa key sha256) header.d=xxxx.com header.i=@xxxx.com header.b=P1aaAAaa x-bits=2048 x-keytype=rsa x-algorithm=sha256 x-selector=fm2;
dmarc=fail (p=none,d=none) header.from=xxxx.com;
iprev=pass policy.iprev=1.1.1.1 (out1-smtp.xxxx.com);
spf=softfail smtp.mailfrom=xxxx@xxxx.com smtp.helo=out1-smtp.xxxx.com;
x-aligned-from=pass (Address match);
x-cm=none score=0;
x-ptr=pass x-ptr-helo=out1-smtp.xxxx.com x-ptr-lookup=out1-smtp.xxxx.com;
x-return-mx=pass smtp.domain=xxxx.com smtp.result=pass smtp_is_org_domain=yes header.domain=xxxx.com header.result=pass header_is_org_domain=yes;
x-tls=pass version=TLSv1.2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128;
x-vs=clean score=0 state=0
Authentication-Results: xxxx.xxxx.com;
arc=none (no signatures found);
dkim=pass (2048-bit rsa key sha256) header.d=xxxx.com header.i=@xxxx.com header.b=P1awJPiy x-bits=2048 x-keytype=rsa x-algorithm=sha256 x-selector=fm2;
dmarc=fail (p=none,d=none) header.from=xxxx.com;
iprev=pass policy.iprev=66.111.4.25 (out1-smtp.xxxx.com);
spf=softfail smtp.mailfrom=xxxx@xxxx.com smtp.helo=out1-smtp.xxxx.com;
x-aligned-from=pass (Address match);
x-cm=none score=0;
x-ptr=pass x-ptr-helo=out1-smtp.xxxx.com x-ptr-lookup=out1-smtp.xxxx.com;
x-return-mx=pass smtp.domain=xxxx.com smtp.result=pass smtp_is_org_domain=yes header.domain=xxxx.com header.result=pass header_is_org_domain=yes;
x-tls=pass version=TLSv1.2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128;
x-vs=clean score=0 state=0
X-ME-VSCategory: clean
X-ME-CMScore: 0
X-ME-CMCategory: none
Received-SPF: softfail
(gmail.com ... _spf.xxxx.com: Sender is not authorized by default to use 'xxxx@xxxx.com' in 'mfrom' identity, however domain is not currently prepared for false failures (mechanism '~all' matched))
receiver=xxxx.xxxx.com;
identity=mailfrom;
envelope-from="xxxx@xxxx.com";
helo=out1-smtp.xxxx.com;
client-ip=1.1.1.1
Received: from xxxx.xxxx.internal (gateway1.xxxx.internal [1.1.1.1])
(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits))
(No client certificate requested)
by xxxx.xxxx.internal (Postfix) with ESMTPS;
Fri, 13 Apr 2018 22:11:51 -0400 (EDT)
Received: from compute3.internal (xxxx.xxxx.internal [10.202.2.43])
by xxxx.xxxx.internal (Postfix) with ESMTP id 8BD5B21BBD;
Fri, 13 Apr 2018 22:11:51 -0400 (EDT)
Received: from xxxx ([10.202.2.163])
by xxxx.internal (MEProxy); Fri, 13 Apr 2018 22:11:51 -0400
X-ME-Sender: <xms:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa>
Received: from [1.1.1.1] (unknown [1.1.1.1])
by mail.xxxx.com (Postfix) with ESMTPA id BF5E1111D
for <catchall@xxxx.xxxx>; Fri, 13 Apr 2018 22:11:50 -0400 (EDT)
From: "xxxx xxxx" <xxxx@xxxx.com>
To: "xxxx" <catchall@xxxx.xxxx>
Subject: Re: xxxx (Ref PO1)
Date: Sat, 14 Apr 2018 02:11:42 +0000
Message-Id: <em67f5c44a-xxxx-xxxx-xxxx-69f56d618a94@wswin7hg4n4l1ce>
In-Reply-To: <829228111124527.1111111602.256611118262939-openerp-129-xxxx.xxxx@ip-1-1-1-1>
References: <867911111953277.1523671337.187951111160400-openerp-129-xxxx.xxxx@ip-1-1-1-1>
<867911111953277.1523671337.256611118262939-openerp-129-xxxx.xxxx@ip-1-1-1-1>
Reply-To: "xxxx xxxx" <xxxx@xxxx.com>
User-Agent: eM_Client/7.0.26687.0
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="------=_MB48E455BD-1111-42EC-1111-886CDF48905E"
--------=_MB48E455BD-1111-42EC-1111-886CDF48905E
Content-Type: text/plain; format=flowed; charset=utf-8
Content-Transfer-Encoding: quoted-printable
xxxx
------ Original Message ------
From: "xxxx" <xxxx@xxxx.com>
To: "xxxx" <xxxx@xxxx.com>
Sent: 4/13/2018 7:06:43 PM
Subject: xxxx
>xxxx
--------=_MB48E455BD-1111-42EC-1111-886CDF48905E
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<?xml version=3D"1.0" encoding=3D"utf-16"?><html><head><style type=3D"text/=
css"><!--blockquote.cite
{margin-left: 5px; margin-right: 0px; padding-left: 10px; padding-right:=
0px; border-left-width: 1px; border-left-style: solid; border-left-color:=
rgb(204, 204, 204);}
blockquote.cite2
{margin-left: 5px; margin-right: 0px; padding-left: 10px; padding-right:=
0px; border-left-width: 1px; border-left-style: solid; border-left-color:=
rgb(204, 204, 204); margin-top: 3px; padding-top: 0px;}
a img
{border: 0px;}
body
{font-family: Tahoma; font-size: 12pt;}
--></style></head><body><div>this is a reply to PO200109 from emClient</div=
><div id=3D"signature_old"><div style=3D"font-family: Tahoma; font-size:=
12 pt;">-- <br /><span><span class=3D"__postbox-detected-content __postbox=
-detected-address" style=3D"TEXT-DECORATION: underline; COLOR: rgb(115,133,=
172); PADDING-BOTTOM: 0pt; PADDING-TOP: 0pt; PADDING-LEFT: 0pt; DISPLAY:=
inline; PADDING-RIGHT: 0pt" __postbox-detected-content=3D"__postbox-detect=
ed-address"></span>xxxx<br />xxxx<br /><b=
r />xxxx</span></=
div></div><div><br /></div><div><br /></div><div><br /></div>
<div>------ Original Message ------</div>
<div>From: "xxxx" &lt;<a href=3D"mailto:xxxx@xxxx.com">xxxx=
@xxxx.com</a>&gt;</div>
<div>To: "xxxx" &lt;<a href=3D"mailto:xxxx@xxxx.com">a=
xxxx@xxxx.com</a>&gt;</div>
<div>Sent: 4/13/2018 7:06:43 PM</div>
<div>Subject: xxxx</div><div><br /></div=
>
<div id=3D"x00b4101ba6e64ce"><blockquote cite=3D"829228972724527.1523671602=
.256660938262939-openerp-129-xxxx.xxxx@ip-1-1-1-1" type=3D"cite"=
class=3D"cite2">
<table border=3D"0" width=3D"100%" cellpadding=3D"0" bgcolor=3D"#ededed"=
style=3D"padding: 20px; background-color: #ededed" summary=3D"o_mail_notif=
ication">
<tbody>
<!-- HEADER -->
<tr>
<td align=3D"center" style=3D"min-width: 590px;">
<table width=3D"590" border=3D"0" cellpadding=3D=
"0" bgcolor=3D"#875A7B" style=3D"min-width: 590px; background-color: rgb(13=
5,90,123); padding: 20px;">
<tbody><tr>
<td valign=3D"middle">
<span style=3D"font-size:20px; color:whit=
e; font-weight: bold;">
mangez des saucisses
</span>
</td>
<td valign=3D"middle" align=3D"right">
<img src=3D"http://erp.xxxx.xxxx/logo.png=
" style=3D"padding: 0px; margin: 0px; height: auto; width: 80px;" alt=3D=
"xxxx" />
</td>
</tr>
</tbody></table>
</td>
</tr>
<!-- CONTENT -->
<tr>
<td align=3D"center" style=3D"min-width: 590px;">
<table width=3D"590" border=3D"0" cellpadding=3D=
"0" bgcolor=3D"#ffffff" style=3D"min-width: 590px; background-color: rgb(25=
5, 255, 255); padding: 20px;">
<tbody>
<tr><td valign=3D"top" style=3D"font-family:A=
rial,Helvetica,sans-serif; color: #555; font-size: 14px;">
<p style=3D"margin: 0px 0px 9px 0px; font-s=
ize: 13px; font-family: &quot;Lucida Grande&quot;, Helvetica, Verdana, Aria=
l, sans-serif">xxxx.=20
,</p>
<p style=3D"margin: 0px 0px 9px 0px; font-size: 13px; font-family: &quot;Lu=
cida Grande&quot;, Helvetica, Verdana, Arial, sans-serif">
xxxx.
</p>
<p style=3D"margin: 0px 0px 9px 0px; font-size: 13px; font-family: &quot;Lu=
cida Grande&quot;, Helvetica, Verdana, Arial, sans-serif">You can reply =
to this email if you have any questions.</p>
<p style=3D"margin: 0px 0px 9px 0px; font-size: 13px; font-family: &quot;Lu=
cida Grande&quot;, Helvetica, Verdana, Arial, sans-serif">Thank you,</p>
</td>
</tr></tbody>
</table>
</td>
</tr>
<!-- FOOTER -->
<tr>
<td align=3D"center" style=3D"min-width: 590px;">
<table width=3D"590" border=3D"0" cellpadding=3D=
"0" bgcolor=3D"#875A7B" style=3D"min-width: 590px; background-color: rgb(13=
5,90,123); padding: 20px;">
<tbody><tr>
<td valign=3D"middle" align=3D"left" style=
=3D"color: #fff; padding-top: 10px; padding-bottom: 10px; font-size: 12px;"=
>
xxxx<br />
+1-801-980-4240
</td>
<td valign=3D"middle" align=3D"right" style=
=3D"color: #fff; padding-top: 10px; padding-bottom: 10px; font-size: 12px;"=
>
<a href=3D"http://erp.xxxx.xxxx/info@xxxx-a=
aa.com" style=3D"text-decoration:none; color: white;">info@aust-mfg.com</a>=
<br />
<a href=3D"http://www.xxxx=
.com" style=3D"text-decoration:none; color: white;">
http://www.xxxx.com
</a>
</td>
</tr>
</tbody></table>
</td>
</tr>
<tr>
<td align=3D"center">
Powered by <a href=3D"https://www.flectrahq.com">Odo=
o</a>.
</td>
</tr>
</tbody>
</table>
=20
<pre style=3D"white-space: pre-wrap">xxxx.
</pre>
</blockquote></div>
</body></html>
--------=_MB48E455BD-2850-42EC-B1CA-886CDF48905E--"""
MAIL_XHTML = """Return-Path: <xxxx@xxxx.com>
Received: from xxxx.internal (xxxx.xxxx.internal [1.1.1.1])
by xxxx (xxxx 1.1.1-111-g972eecc-slipenbois) with LMTPA;
Fri, 13 Apr 2018 22:11:52 -0400
X-Cyrus-Session-Id: sloti35d1t38-1111111-11111111111-5-11111111111111111111
X-Sieve: CMU Sieve 1.0
X-Spam-known-sender: no ("Email failed DMARC policy for domain"); in-addressbook
X-Spam-score: 0.0
X-Spam-hits: ALL_TRUSTED -1, BAYES_00 -1.9, FREEMAIL_FROM 0.001,
HTML_FONT_LOW_CONTRAST 0.001, HTML_MESSAGE 0.001, SPF_SOFTFAIL 0.665,
LANGUAGES en, BAYES_USED global, SA_VERSION 1.1.0
X-Spam-source: IP='1.1.1.1', Host='unk', Country='unk', FromHeader='com',
MailFrom='com'
X-Spam-charsets: plain='utf-8', html='utf-8'
X-IgnoreVacation: yes ("Email failed DMARC policy for domain")
X-Resolved-to: catchall@xxxx.xxxx
X-Delivered-to: catchall@xxxx.xxxx
X-Mail-from: xxxx@xxxx.com
Received: from mx4 ([1.1.1.1])
by xxxx.internal (LMTPProxy); Fri, 13 Apr 2018 22:11:52 -0400
Received: from xxxx.xxxx.com (localhost [127.0.0.1])
by xxxx.xxxx.internal (Postfix) with ESMTP id E1111C1111;
Fri, 13 Apr 2018 22:11:51 -0400 (EDT)
Received: from xxxx.xxxx.internal (localhost [127.0.0.1])
by xxxx.xxxx.com (Authentication Milter) with ESMTP
id BBDD1111D1A;
Fri, 13 Apr 2018 22:11:51 -0400
ARC-Authentication-Results: i=1; xxxx.xxxx.com; arc=none (no signatures found);
dkim=pass (2048-bit rsa key sha256) header.d=xxxx.com header.i=@xxxx.com header.b=P1aaAAaa x-bits=2048 x-keytype=rsa x-algorithm=sha256 x-selector=fm2;
dmarc=fail (p=none,d=none) header.from=xxxx.com;
iprev=pass policy.iprev=1.1.1.1 (out1-smtp.xxxx.com);
spf=softfail smtp.mailfrom=xxxx@xxxx.com smtp.helo=out1-smtp.xxxx.com;
x-aligned-from=pass (Address match);
x-cm=none score=0;
x-ptr=pass x-ptr-helo=out1-smtp.xxxx.com x-ptr-lookup=out1-smtp.xxxx.com;
x-return-mx=pass smtp.domain=xxxx.com smtp.result=pass smtp_is_org_domain=yes header.domain=xxxx.com header.result=pass header_is_org_domain=yes;
x-tls=pass version=TLSv1.2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128;
x-vs=clean score=0 state=0
Authentication-Results: xxxx.xxxx.com;
arc=none (no signatures found);
dkim=pass (2048-bit rsa key sha256) header.d=xxxx.com header.i=@xxxx.com header.b=P1awJPiy x-bits=2048 x-keytype=rsa x-algorithm=sha256 x-selector=fm2;
dmarc=fail (p=none,d=none) header.from=xxxx.com;
iprev=pass policy.iprev=66.111.4.25 (out1-smtp.xxxx.com);
spf=softfail smtp.mailfrom=xxxx@xxxx.com smtp.helo=out1-smtp.xxxx.com;
x-aligned-from=pass (Address match);
x-cm=none score=0;
x-ptr=pass x-ptr-helo=out1-smtp.xxxx.com x-ptr-lookup=out1-smtp.xxxx.com;
x-return-mx=pass smtp.domain=xxxx.com smtp.result=pass smtp_is_org_domain=yes header.domain=xxxx.com header.result=pass header_is_org_domain=yes;
x-tls=pass version=TLSv1.2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128;
x-vs=clean score=0 state=0
X-ME-VSCategory: clean
X-ME-CMScore: 0
X-ME-CMCategory: none
Received-SPF: softfail
(gmail.com ... _spf.xxxx.com: Sender is not authorized by default to use 'xxxx@xxxx.com' in 'mfrom' identity, however domain is not currently prepared for false failures (mechanism '~all' matched))
receiver=xxxx.xxxx.com;
identity=mailfrom;
envelope-from="xxxx@xxxx.com";
helo=out1-smtp.xxxx.com;
client-ip=1.1.1.1
Received: from xxxx.xxxx.internal (gateway1.xxxx.internal [1.1.1.1])
(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits))
(No client certificate requested)
by xxxx.xxxx.internal (Postfix) with ESMTPS;
Fri, 13 Apr 2018 22:11:51 -0400 (EDT)
Received: from compute3.internal (xxxx.xxxx.internal [10.202.2.43])
by xxxx.xxxx.internal (Postfix) with ESMTP id 8BD5B21BBD;
Fri, 13 Apr 2018 22:11:51 -0400 (EDT)
Received: from xxxx ([10.202.2.163])
by xxxx.internal (MEProxy); Fri, 13 Apr 2018 22:11:51 -0400
X-ME-Sender: <xms:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa>
Received: from [1.1.1.1] (unknown [1.1.1.1])
by mail.xxxx.com (Postfix) with ESMTPA id BF5E1111D
for <catchall@xxxx.xxxx>; Fri, 13 Apr 2018 22:11:50 -0400 (EDT)
From: "xxxx xxxx" <xxxx@xxxx.com>
To: "xxxx" <catchall@xxxx.xxxx>
Subject: Re: xxxx (Ref PO1)
Date: Sat, 14 Apr 2018 02:11:42 +0000
Message-Id: <em67f5c44a-xxxx-xxxx-xxxx-69f56d618a94@wswin7hg4n4l1ce>
In-Reply-To: <829228111124527.1111111602.256611118262939-openerp-129-xxxx.xxxx@ip-1-1-1-1>
References: <867911111953277.1523671337.187951111160400-openerp-129-xxxx.xxxx@ip-1-1-1-1>
<867911111953277.1523671337.256611118262939-openerp-129-xxxx.xxxx@ip-1-1-1-1>
Reply-To: "xxxx xxxx" <xxxx@xxxx.com>
User-Agent: eM_Client/7.0.26687.0
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="------=_MB48E455BD-1111-42EC-1111-886CDF48905E"
--------=_MB48E455BD-1111-42EC-1111-886CDF48905E
Content-Type: text/plain; format=flowed; charset=utf-8
Content-Transfer-Encoding: quoted-printable
xxxx
------ Original Message ------
From: "xxxx" <xxxx@xxxx.com>
To: "xxxx" <xxxx@xxxx.com>
Sent: 4/13/2018 7:06:43 PM
Subject: xxxx
>xxxx
--------=_MB48E455BD-1111-42EC-1111-886CDF48905E
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<?xml version=3D"1.0" encoding=3D"utf-16"?><html><head><style type=3D"text/=
css"><!--blockquote.cite
{margin-left: 5px; margin-right: 0px; padding-left: 10px; padding-right:=
0px; border-left-width: 1px; border-left-style: solid; border-left-color:=
rgb(204, 204, 204);}
blockquote.cite2
{margin-left: 5px; margin-right: 0px; padding-left: 10px; padding-right:=
0px; border-left-width: 1px; border-left-style: solid; border-left-color:=
rgb(204, 204, 204); margin-top: 3px; padding-top: 0px;}
a img
{border: 0px;}
body
{font-family: Tahoma; font-size: 12pt;}
--></style></head><body><div>this is a reply to PO200109 from emClient</div=
><div id=3D"signature_old"><div style=3D"font-family: Tahoma; font-size:=
12 pt;">-- <br /><span><span class=3D"__postbox-detected-content __postbox=
-detected-address" style=3D"TEXT-DECORATION: underline; COLOR: rgb(115,133,=
172); PADDING-BOTTOM: 0pt; PADDING-TOP: 0pt; PADDING-LEFT: 0pt; DISPLAY:=
inline; PADDING-RIGHT: 0pt" __postbox-detected-content=3D"__postbox-detect=
ed-address"></span>xxxx<br />xxxx<br /><b=
r />xxxx</span></=
div></div><div><br /></div><div><br /></div><div><br /></div>
<div>------ Original Message ------</div>
<div>From: "xxxx" &lt;<a href=3D"mailto:xxxx@xxxx.com">xxxx=
@xxxx.com</a>&gt;</div>
<div>To: "xxxx" &lt;<a href=3D"mailto:xxxx@xxxx.com">a=
xxxx@xxxx.com</a>&gt;</div>
<div>Sent: 4/13/2018 7:06:43 PM</div>
<div>Subject: xxxx</div><div><br /></div=
>
<div id=3D"x00b4101ba6e64ce"><blockquote cite=3D"829228972724527.1523671602=
.256660938262939-openerp-129-xxxx.xxxx@ip-1-1-1-1" type=3D"cite"=
class=3D"cite2">
<table border=3D"0" width=3D"100%" cellpadding=3D"0" bgcolor=3D"#ededed"=
style=3D"padding: 20px; background-color: #ededed" summary=3D"o_mail_notif=
ication">
<tbody>
<!-- HEADER -->
<tr>
<td align=3D"center" style=3D"min-width: 590px;">
<table width=3D"590" border=3D"0" cellpadding=3D=
"0" bgcolor=3D"#875A7B" style=3D"min-width: 590px; background-color: rgb(13=
5,90,123); padding: 20px;">
<tbody><tr>
<td valign=3D"middle">
<span style=3D"font-size:20px; color:whit=
e; font-weight: bold;">
mangez des saucisses
</span>
</td>
<td valign=3D"middle" align=3D"right">
<img src=3D"http://erp.xxxx.xxxx/logo.png=
" style=3D"padding: 0px; margin: 0px; height: auto; width: 80px;" alt=3D=
"xxxx" />
</td>
</tr>
</tbody></table>
</td>
</tr>
<!-- CONTENT -->
<tr>
<td align=3D"center" style=3D"min-width: 590px;">
<table width=3D"590" border=3D"0" cellpadding=3D=
"0" bgcolor=3D"#ffffff" style=3D"min-width: 590px; background-color: rgb(25=
5, 255, 255); padding: 20px;">
<tbody>
<tr><td valign=3D"top" style=3D"font-family:A=
rial,Helvetica,sans-serif; color: #555; font-size: 14px;">
<p style=3D"margin: 0px 0px 9px 0px; font-s=
ize: 13px; font-family: &quot;Lucida Grande&quot;, Helvetica, Verdana, Aria=
l, sans-serif">xxxx.=20
,</p>
<p style=3D"margin: 0px 0px 9px 0px; font-size: 13px; font-family: &quot;Lu=
cida Grande&quot;, Helvetica, Verdana, Arial, sans-serif">
xxxx.
</p>
<p style=3D"margin: 0px 0px 9px 0px; font-size: 13px; font-family: &quot;Lu=
cida Grande&quot;, Helvetica, Verdana, Arial, sans-serif">You can reply =
to this email if you have any questions.</p>
<p style=3D"margin: 0px 0px 9px 0px; font-size: 13px; font-family: &quot;Lu=
cida Grande&quot;, Helvetica, Verdana, Arial, sans-serif">Thank you,</p>
</td>
</tr></tbody>
</table>
</td>
</tr>
<!-- FOOTER -->
<tr>
<td align=3D"center" style=3D"min-width: 590px;">
<table width=3D"590" border=3D"0" cellpadding=3D=
"0" bgcolor=3D"#875A7B" style=3D"min-width: 590px; background-color: rgb(13=
5,90,123); padding: 20px;">
<tbody><tr>
<td valign=3D"middle" align=3D"left" style=
=3D"color: #fff; padding-top: 10px; padding-bottom: 10px; font-size: 12px;"=
>
xxxx<br />
+1-801-980-4240
</td>
<td valign=3D"middle" align=3D"right" style=
=3D"color: #fff; padding-top: 10px; padding-bottom: 10px; font-size: 12px;"=
>
<a href=3D"http://erp.xxxx.xxxx/info@xxxx-a=
aa.com" style=3D"text-decoration:none; color: white;">info@aust-mfg.com</a>=
<br />
<a href=3D"http://www.xxxx=
.com" style=3D"text-decoration:none; color: white;">
http://www.xxxx.com
</a>
</td>
</tr>
</tbody></table>
</td>
</tr>
<tr>
<td align=3D"center">
Powered by <a href=3D"https://www.flectrahq.com">Odo=
o</a>.
</td>
</tr>
</tbody>
</table>
=20
<pre style=3D"white-space: pre-wrap">xxxx.
</pre>
</blockquote></div>
</body></html>
--------=_MB48E455BD-2850-42EC-B1CA-886CDF48905E--"""
class TestMailgateway(TestMail): class TestMailgateway(TestMail):
@ -327,7 +951,7 @@ class TestMailgateway(TestMail):
'subject': 'Public Discussion', 'subject': 'Public Discussion',
'message_type': 'email', 'message_type': 'email',
'author_id': self.partner_1.id, 'author_id': self.partner_1.id,
'message_id': '<123456-flectra-%s-mail.test@%s>' % (self.test_public.id, socket.gethostname()), 'message_id': '<123456-openerp-%s-mail.test@%s>' % (self.test_public.id, socket.gethostname()),
}) })
@mute_logger('flectra.addons.mail.models.mail_thread') @mute_logger('flectra.addons.mail.models.mail_thread')
@ -366,6 +990,12 @@ class TestMailgateway(TestMail):
self.assertEqual(res['body'], '') self.assertEqual(res['body'], '')
self.assertEqual(res['attachments'][0][0], 'thetruth.pdf') self.assertEqual(res['attachments'][0][0], 'thetruth.pdf')
@mute_logger('flectra.addons.mail.models.mail_thread')
def test_message_parse_xhtml(self):
""" Test that the parsing of mail with embedded emails as eml(msg) which generates empty attachments, can be processed.
"""
self.env['mail.thread'].message_process('mail.channel', MAIL_XHTML)
@mute_logger('flectra.addons.mail.models.mail_thread') @mute_logger('flectra.addons.mail.models.mail_thread')
def test_message_process_cid(self): def test_message_process_cid(self):
new_groups = self.format_and_process(MAIL_MULTIPART_IMAGE, subject='My Frogs', to='groups@example.com') new_groups = self.format_and_process(MAIL_MULTIPART_IMAGE, subject='My Frogs', to='groups@example.com')
@ -528,13 +1158,7 @@ class TestMailgateway(TestMail):
'message_process: email should be sent to Sylvie') 'message_process: email should be sent to Sylvie')
# TODO : the author of a message post on mail.test should not be added as follower # TODO : the author of a message post on mail.test should not be added as follower
# FAIL ON 'message_process: after reply, group should have 2 followers') ` AssertionError: res.partner(104,) != res.partner(104, 105) : message_process: after reply, group should have 2 followers
# Test: author (and not recipient) added as follower
# self.assertEqual(self.test_public.message_partner_ids, self.partner_1 | self.partner_2,
# 'message_process: after reply, group should have 2 followers')
# self.assertEqual(self.test_public.message_channel_ids, self.env['mail.test'],
# 'message_process: after reply, group should have 2 followers (0 channels)')
@mute_logger('flectra.addons.mail.models.mail_thread', 'flectra.models') @mute_logger('flectra.addons.mail.models.mail_thread', 'flectra.models')
def test_message_process_in_reply_to(self): def test_message_process_in_reply_to(self):
@ -569,6 +1193,7 @@ class TestMailgateway(TestMail):
MAIL_TEMPLATE, to='erroneous@example.com', MAIL_TEMPLATE, to='erroneous@example.com',
extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id, extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id,
msg_id='<1198923581.41972151344608186800.JavaMail.4@agrolait.com>') msg_id='<1198923581.41972151344608186800.JavaMail.4@agrolait.com>')
self.assertEqual(len(self.test_public.message_ids), 2, 'message_process: group should contain one new message') self.assertEqual(len(self.test_public.message_ids), 2, 'message_process: group should contain one new message')
self.assertEqual(len(self.fake_email.child_ids), 1, 'message_process: new message should be children of the existing one') self.assertEqual(len(self.fake_email.child_ids), 1, 'message_process: new message should be children of the existing one')
@ -581,10 +1206,10 @@ class TestMailgateway(TestMail):
extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id, extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id,
msg_id='<1198923581.41972151344608186800.JavaMail.4@agrolait.com>', msg_id='<1198923581.41972151344608186800.JavaMail.4@agrolait.com>',
target_model='mail.channel') target_model='mail.channel')
self.assertEqual(len(self.test_public.message_ids), 1, 'message_process: group should not contain new message')
self.assertEqual(len(self.fake_email.child_ids), 0, 'message_process: original email should not contain childs') self.assertEqual(len(self.test_public.message_ids), 2, 'message_process: group should not contain new message')
self.assertEqual(res_test.name, 'My Dear Forward') self.assertEqual(len(self.fake_email.child_ids), 1, 'message_process: original email should not contain childs')
self.assertEqual(len(res_test.message_ids), 1) self.assertEqual(len(res_test.message_ids), 0)
@mute_logger('flectra.addons.mail.models.mail_thread', 'flectra.models') @mute_logger('flectra.addons.mail.models.mail_thread', 'flectra.models')
def test_message_process_references_forward_cc(self): def test_message_process_references_forward_cc(self):
@ -606,11 +1231,11 @@ class TestMailgateway(TestMail):
self.format_and_process, self.format_and_process,
MAIL_TEMPLATE, email_from='valid.lelitre@agrolait.com', MAIL_TEMPLATE, email_from='valid.lelitre@agrolait.com',
to='noone@example.com', subject='spam', to='noone@example.com', subject='spam',
extra='In-Reply-To: <12321321-flectra-%d-mail.test@%s>' % (self.test_public.id, socket.gethostname()), extra='In-Reply-To: <12321321-openerp-%d-mail.test@%s>' % (self.test_public.id, socket.gethostname()),
msg_id='<1198923581.41972151344608186802.JavaMail.diff1@agrolait.com>') msg_id='<1198923581.41972151344608186802.JavaMail.diff1@agrolait.com>')
# when 6.1 messages are present, compat mode is available # when 6.1 messages are present, compat mode is available
# Flectra 10 update: compat mode has been removed and should not work anymore # Odoo 10 update: compat mode has been removed and should not work anymore
self.fake_email.write({'message_id': False}) self.fake_email.write({'message_id': False})
# Do: compat mode accepts partial-matching emails # Do: compat mode accepts partial-matching emails
self.assertRaises( self.assertRaises(
@ -619,16 +1244,16 @@ class TestMailgateway(TestMail):
MAIL_TEMPLATE, email_from='other5@gmail.com', MAIL_TEMPLATE, email_from='other5@gmail.com',
msg_id='<1.2.JavaMail.new@agrolait.com>', msg_id='<1.2.JavaMail.new@agrolait.com>',
to='noone@example.com>', subject='spam', to='noone@example.com>', subject='spam',
extra='In-Reply-To: <12321321-flectra-%d-mail.test@%s>' % (self.test_public.id, socket.gethostname())) extra='In-Reply-To: <12321321-openerp-%d-mail.test@%s>' % (self.test_public.id, socket.gethostname()))
# 3''. 6.1 compat mode should not work if hostname does not match! # 3''. 6.1 compat mode should not work if hostname does not match!
# Flectra 10 update: compat mode has been removed and should not work anymore and does not depend from hostname # Odoo 10 update: compat mode has been removed and should not work anymore and does not depend from hostname
self.assertRaises(ValueError, self.assertRaises(ValueError,
self.format_and_process, self.format_and_process,
MAIL_TEMPLATE, email_from='other5@gmail.com', MAIL_TEMPLATE, email_from='other5@gmail.com',
msg_id='<1.3.JavaMail.new@agrolait.com>', msg_id='<1.3.JavaMail.new@agrolait.com>',
to='noone@example.com>', subject='spam', to='noone@example.com>', subject='spam',
extra='In-Reply-To: <12321321-flectra-%d-mail.test@neighbor.com>' % self.test_public.id) extra='In-Reply-To: <12321321-openerp-%d-mail.test@neighbor.com>' % self.test_public.id)
# Test created messages # Test created messages
self.assertEqual(len(self.test_public.message_ids), 1) self.assertEqual(len(self.test_public.message_ids), 1)

View File

@ -23,7 +23,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-xs-12 col-md-6 o_setting_box" title="Using your own email server is required to send/receive emails in Community and Enterprise versions. Online users already benefit from a ready-to-use email server (@mycompany.flectra.com)."> <div class="col-xs-12 col-md-6 o_setting_box" title="Using your own email server is required to send/receive emails in Community versions. Online users already benefit from a ready-to-use email server (@mycompany.flectrahq.com).">
<div class="o_setting_left_pane"> <div class="o_setting_left_pane">
<field name="default_external_email_server"/> <field name="default_external_email_server"/>
</div> </div>

View File

@ -11,6 +11,11 @@ from flectra.tools import consteq
class MassMailController(http.Controller): class MassMailController(http.Controller):
@http.route(['/unsubscribe_from_list'], type='http', website=True, multilang=False, auth='public')
def unsubscribe_placeholder_link(self, **post):
"""Dummy route so placeholder is not prefixed by language, MUST have multilang=False"""
raise werkzeug.exceptions.NotFound()
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public') @http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
def mailing(self, mailing_id, email=None, res_id=None, token="", **post): def mailing(self, mailing_id, email=None, res_id=None, token="", **post):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id) mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)

View File

@ -92,7 +92,7 @@ class MailMail(models.Model):
def send_get_email_dict(self, partner=None): def send_get_email_dict(self, partner=None):
# TDE: temporary addition (mail was parameter) due to semi-new-API # TDE: temporary addition (mail was parameter) due to semi-new-API
res = super(MailMail, self).send_get_email_dict(partner) res = super(MailMail, self).send_get_email_dict(partner)
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url').rstrip('/')
if self.mailing_id and res.get('body') and res.get('email_to'): if self.mailing_id and res.get('body') and res.get('email_to'):
emails = tools.email_split(res.get('email_to')[0]) emails = tools.email_split(res.get('email_to')[0])
email_to = emails and emails[0] or False email_to = emails and emails[0] or False

View File

@ -681,6 +681,7 @@ class MassMailing(models.Model):
'mass_mailing_id': mailing.id, 'mass_mailing_id': mailing.id,
'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids], 'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
'no_auto_thread': mailing.reply_to_mode != 'thread', 'no_auto_thread': mailing.reply_to_mode != 'thread',
'template_id': None,
} }
if mailing.reply_to_mode == 'email': if mailing.reply_to_mode == 'email':
composer_values['reply_to'] = mailing.reply_to composer_values['reply_to'] = mailing.reply_to

View File

@ -38,7 +38,7 @@ FieldTextHtml.include({
var datarecord = this._super(); var datarecord = this._super();
if (this.model === 'mail.mass_mailing') { if (this.model === 'mail.mass_mailing') {
// these fields can potentially get very long, let's remove them // these fields can potentially get very long, let's remove them
datarecord = _.omit(datarecord, ['mailing_domain', 'contact_list_ids', 'body_html']); datarecord = _.omit(datarecord, ['mailing_domain', 'contact_list_ids', 'body_html', 'attachment_ids']);
} }
return datarecord; return datarecord;
}, },

View File

@ -46,6 +46,11 @@
background-color: @o_mm_def_color_alpha; background-color: @o_mm_def_color_alpha;
} }
td {
// Default browser style but needed so that alignment works on some mail
// clients (see transcoder)
text-align: inherit;
}
// ===== Layout ===== // ===== Layout =====
.o_layout { .o_layout {

View File

@ -26,7 +26,7 @@ class TestMassMailing(models.TransientModel):
'reply_to': mailing.reply_to, 'reply_to': mailing.reply_to,
'email_to': test_mail, 'email_to': test_mail,
'subject': mailing.name, 'subject': mailing.name,
'body_html': mailing.body_html, 'body_html': tools.html_sanitize(mailing.body_html, sanitize_attributes=True, sanitize_style=True, strip_classes=True),
'notification': True, 'notification': True,
'mailing_id': mailing.id, 'mailing_id': mailing.id,
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids], 'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],

View File

@ -10,7 +10,9 @@
name="action_mass_mailing_attendees" name="action_mass_mailing_attendees"
icon="fa-envelope-o" icon="fa-envelope-o"
attrs="{'invisible': [('seats_expected', '=', 0)]}"> attrs="{'invisible': [('seats_expected', '=', 0)]}">
Mail Attendees <div class="o_field_widget o_stat_info">
<span class="o_stat_text">Mail Attendees</span>
</div>
</button> </button>
</button> </button>
</field> </field>

View File

@ -16,7 +16,7 @@ class Event(models.Model):
view_mode='form', view_mode='form',
target='current', target='current',
context=dict( context=dict(
default_mailing_model=self.env.ref('website_event_track.model_event_track').id, default_mailing_model_id=self.env.ref('website_event_track.model_event_track').id,
default_mailing_domain="[('event_id', 'in', %s), ('stage_id.is_cancel', '!=', True)]" % self.ids, default_mailing_domain="[('event_id', 'in', %s), ('stage_id.is_cancel', '!=', True)]" % self.ids,
), ),
) )

View File

@ -100,7 +100,7 @@ class PortalChatter(http.Controller):
raise Forbidden() raise Forbidden()
# Non-employee see only messages with not internal subtype (aka, no internal logs) # Non-employee see only messages with not internal subtype (aka, no internal logs)
if not request.env['res.users'].has_group('base.group_user'): if not request.env['res.users'].has_group('base.group_user'):
domain = expression.AND([['&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)], domain]) domain = expression.AND([['&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)], domain])
Message = request.env['mail.message'].sudo() Message = request.env['mail.message'].sudo()
return { return {
'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(), 'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(),

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import ir_http
from . import mail_thread from . import mail_thread
from . import mail_message from . import mail_message
from . import portal_mixin from . import portal_mixin

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import api, fields, models
from flectra.osv import expression
class IrHttp(models.AbstractModel):
_inherit = 'ir.http'
@classmethod
def _get_translation_frontend_modules_domain(cls):
domain = super(IrHttp, cls)._get_translation_frontend_modules_domain()
return expression.OR([domain, [('name', '=', 'portal')]])

View File

@ -10,6 +10,7 @@
is_website_user: <t t-esc="json.dumps(request.env.user._is_public())"/>, is_website_user: <t t-esc="json.dumps(request.env.user._is_public())"/>,
user_id: <t t-esc="json.dumps(request.env.user.id)" />, user_id: <t t-esc="json.dumps(request.env.user.id)" />,
is_frontend: true, is_frontend: true,
translationURL: '/website/translations',
}; };
</script> </script>
@ -194,6 +195,21 @@
</li> </li>
</ul> </ul>
</div> </div>
<div t-if="searchbar_groupby" class="dropdown pull-right mr4">
<button id="portal_searchbar_groupby" class="o_portal_search_panel_fixed_width btn btn-default" type="button" data-toggle="dropdown">
<span class="fa fa-bars fa-lg" />
<span class='hidden-xs hidden-sm hidden-md' t-esc="searchbar_groupby[groupby].get('label', 'None')"/>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="portal_searchbar_groupby">
<li t-foreach="searchbar_groupby" t-as="option" t-att-class="groupby == option and 'active'">
<a t-att-href="default_url + '?' + keep_query('*', groupby=option)">
<span t-esc="searchbar_groupby[option].get('label')"/>
</a>
</li>
</ul>
</div>
</template> </template>
<template id="portal_contact" name="Contact"> <template id="portal_contact" name="Contact">

View File

@ -140,7 +140,7 @@ class PortalWizardUser(models.TransientModel):
if wizard_user.partner_id.company_id: if wizard_user.partner_id.company_id:
company_id = wizard_user.partner_id.company_id.id company_id = wizard_user.partner_id.company_id.id
else: else:
company_id = self.env['res.company']._company_default_get('res.users') company_id = self.env['res.company']._company_default_get('res.users').id
user_portal = wizard_user.sudo().with_context(company_id=company_id)._create_user() user_portal = wizard_user.sudo().with_context(company_id=company_id)._create_user()
else: else:
user_portal = user user_portal = user