From fbafbf2230c485ea04ab03c778455bb59389c7d9 Mon Sep 17 00:00:00 2001 From: XavierDo Date: Tue, 19 Feb 2019 14:30:18 +0000 Subject: [PATCH] [IMP] mail, test_mail, *: add new email.cc.mixin on main models *: crm, project, maintenance, hr_recruitment When a record is created from an email, cc can be lost. This commit proposes a new mixin to keep cc on the record and allows to create partner for each of them when sending a message from the chatter. The mixin is added on the mail recors that can be created from mail.alias: document helpdesk.ticket mrp.eco quality.alert hr.applicant crm.lead project.task mail.channel maintenance.equipment Task: 1925001 --- addons/crm/models/crm_lead.py | 4 +- addons/crm/views/crm_lead_views.xml | 1 + .../hr_recruitment/models/hr_recruitment.py | 5 +- .../views/hr_recruitment_views.xml | 1 + addons/mail/models/__init__.py | 1 + addons/mail/models/mail_cc_mixin.py | 53 +++++++++++++++ addons/maintenance/models/maintenance.py | 5 +- .../maintenance/views/maintenance_views.xml | 1 + addons/project/models/project.py | 5 +- addons/test_mail/models/test_mail_models.py | 7 ++ addons/test_mail/security/ir.model.access.csv | 3 + addons/test_mail/tests/__init__.py | 1 + addons/test_mail/tests/test_mail_cc.py | 66 +++++++++++++++++++ odoo/tools/mail.py | 19 +++--- 14 files changed, 150 insertions(+), 22 deletions(-) create mode 100644 addons/mail/models/mail_cc_mixin.py create mode 100644 addons/test_mail/tests/test_mail_cc.py diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index ca59e30798b..6933420c951 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -50,7 +50,7 @@ class Lead(models.Model): _name = "crm.lead" _description = "Lead/Opportunity" _order = "priority desc,activity_date_deadline,id desc" - _inherit = ['mail.thread', 'mail.activity.mixin', 'utm.mixin', 'format.address.mixin', 'mail.blacklist.mixin'] + _inherit = ['mail.thread.cc', 'mail.activity.mixin', 'utm.mixin', 'format.address.mixin', 'mail.blacklist.mixin'] _primary_email = 'email_from' def _default_probability(self): @@ -74,7 +74,6 @@ class Lead(models.Model): index=True, tracking=True, help='When sending mails, the default email address is taken from the Sales Team.') kanban_state = fields.Selection([('grey', 'No next activity planned'), ('red', 'Next activity late'), ('green', 'Next activity is planned')], string='Kanban State', compute='_compute_kanban_state') - email_cc = fields.Text('Global CC', help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma") description = fields.Text('Notes') tag_ids = fields.Many2many('crm.lead.tag', 'crm_lead_tag_rel', 'lead_id', 'tag_id', string='Tags', help="Classify and analyze your lead/opportunity categories like: Training, Service") contact_name = fields.Char('Contact Name', tracking=30) @@ -1204,7 +1203,6 @@ class Lead(models.Model): defaults = { 'name': msg_dict.get('subject') or _("No Subject"), 'email_from': msg_dict.get('from'), - 'email_cc': msg_dict.get('cc'), 'partner_id': msg_dict.get('author_id', False), } if msg_dict.get('author_id'): diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 4abe323d06e..78bc490c674 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -107,6 +107,7 @@ aria-label="Blacklisted" attrs="{'invisible': [('is_blacklisted', '=', False)]}" groups="base.group_user"> + diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py index f4338dc24f5..f9c057f1172 100644 --- a/addons/hr_recruitment/models/hr_recruitment.py +++ b/addons/hr_recruitment/models/hr_recruitment.py @@ -92,7 +92,7 @@ class Applicant(models.Model): _name = "hr.applicant" _description = "Applicant" _order = "priority desc, id desc" - _inherit = ['mail.thread', 'mail.activity.mixin', 'utm.mixin'] + _inherit = ['mail.thread.cc', 'mail.activity.mixin', 'utm.mixin'] def _default_stage_id(self): if self._context.get('default_job_id'): @@ -117,8 +117,6 @@ class Applicant(models.Model): active = fields.Boolean("Active", default=True, help="If the active field is set to false, it will allow you to hide the case without removing it.") description = fields.Text("Description") email_from = fields.Char("Email", size=128, help="Applicant email") - email_cc = fields.Text("Watchers Emails", size=252, - help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma") probability = fields.Float("Probability") partner_id = fields.Many2one('res.partner', "Contact") create_date = fields.Datetime("Creation Date", readonly=True, index=True) @@ -407,7 +405,6 @@ class Applicant(models.Model): 'name': msg.get('subject') or _("No Subject"), 'partner_name': val, 'email_from': msg.get('from'), - 'email_cc': msg.get('cc'), 'partner_id': msg.get('author_id', False), } if msg.get('priority'): diff --git a/addons/hr_recruitment/views/hr_recruitment_views.xml b/addons/hr_recruitment/views/hr_recruitment_views.xml index b298ebc7bb1..b2f81225d04 100644 --- a/addons/hr_recruitment/views/hr_recruitment_views.xml +++ b/addons/hr_recruitment/views/hr_recruitment_views.xml @@ -131,6 +131,7 @@ + diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py index 750446371ee..d0e1d954e63 100644 --- a/addons/mail/models/__init__.py +++ b/addons/mail/models/__init__.py @@ -10,6 +10,7 @@ from . import mail_message from . import mail_activity from . import mail_mail from . import mail_thread +from . import mail_cc_mixin from . import mail_address_mixin from . import mail_blacklist from . import mail_channel diff --git a/addons/mail/models/mail_cc_mixin.py b/addons/mail/models/mail_cc_mixin.py new file mode 100644 index 00000000000..bf682c7d4dd --- /dev/null +++ b/addons/mail/models/mail_cc_mixin.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, api, fields, models, tools +from email.utils import formataddr + + +class MailCCMixin(models.AbstractModel): + _name = 'mail.thread.cc' + _inherit = 'mail.thread' + _description = 'Email CC management' + + email_cc = fields.Char('Email cc', help='List of cc from incoming emails.') + + def _mail_cc_sanitized_raw_dict(self, cc_string): + '''return a dict of sanitize_email:raw_email from a string of cc''' + if not cc_string: + return {} + return {tools.email_normalize(email): formataddr((name, tools.email_normalize(email))) + for (name, email) in tools.email_split_tuples(cc_string)} + + @api.model + def message_new(self, msg_dict, custom_values=None): + if custom_values is None: + custom_values = {} + cc_values = { + 'email_cc': ", ".join(self._mail_cc_sanitized_raw_dict(msg_dict.get('cc')).values()), + } + cc_values.update(custom_values) + return super(MailCCMixin, self).message_new(msg_dict, cc_values) + + @api.multi + def message_update(self, msg_dict, update_vals=None): + '''Adds cc email to self.email_cc while trying to keep email as raw as possible but unique''' + if update_vals is None: + update_vals = {} + cc_values = {} + new_cc = self._mail_cc_sanitized_raw_dict(msg_dict.get('cc')) + if new_cc: + old_cc = self._mail_cc_sanitized_raw_dict(self.email_cc) + new_cc.update(old_cc) + cc_values['email_cc'] = ", ".join(new_cc.values()) + cc_values.update(update_vals) + return super(MailCCMixin, self).message_update(msg_dict, cc_values) + + @api.multi + def message_get_suggested_recipients(self): + recipients = super(MailCCMixin, self).message_get_suggested_recipients() + for record in self: + if record.email_cc: + for email in tools.email_split_and_format(record.email_cc): + record._message_add_suggested_recipient(recipients, email=email, reason=_('CC Email')) + return recipients diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py index cd7c3a69120..fcb3436940c 100644 --- a/addons/maintenance/models/maintenance.py +++ b/addons/maintenance/models/maintenance.py @@ -81,7 +81,7 @@ class MaintenanceEquipmentCategory(models.Model): return res def get_alias_model_name(self, vals): - return vals.get('alias_model', 'maintenance.equipment') + return vals.get('alias_model', 'maintenance.request') def get_alias_values(self): values = super(MaintenanceEquipmentCategory, self).get_alias_values() @@ -253,9 +253,10 @@ class MaintenanceEquipment(models.Model): if not next_requests: equipment._create_new_request(equipment.next_action_date) + class MaintenanceRequest(models.Model): _name = 'maintenance.request' - _inherit = ['mail.thread', 'mail.activity.mixin'] + _inherit = ['mail.thread.cc', 'mail.activity.mixin'] _description = 'Maintenance Request' _order = "id desc" diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml index 6723190437d..0e89fa52009 100644 --- a/addons/maintenance/views/maintenance_views.xml +++ b/addons/maintenance/views/maintenance_views.xml @@ -94,6 +94,7 @@ class="oe_inline"/> hours + diff --git a/addons/project/models/project.py b/addons/project/models/project.py index e2b9510c1d8..49e58b53a67 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -399,7 +399,7 @@ class Task(models.Model): _name = "project.task" _description = "Task" _date_name = "date_assign" - _inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin', 'rating.mixin'] + _inherit = ['portal.mixin', 'mail.thread.cc', 'mail.activity.mixin', 'rating.mixin'] _mail_post_access = 'read' _order = "priority desc, sequence, id desc" @@ -496,8 +496,6 @@ class Task(models.Model): subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project', readonly=True) subtask_count = fields.Integer("Sub-task count", compute='_compute_subtask_count') email_from = fields.Char(string='Email', help="These people will receive email.", index=True) - email_cc = fields.Char(string='Watchers Emails', help="""These email addresses will be added to the CC field of all inbound - and outbound emails for this record before being sent. Separate multiple email addresses with a comma""") # Computed field about working time elapsed between record creation and assignation/closing. working_hours_open = fields.Float(compute='_compute_elapsed', string='Working hours to assign', store=True, group_operator="avg") working_hours_close = fields.Float(compute='_compute_elapsed', string='Working hours to close', store=True, group_operator="avg") @@ -835,7 +833,6 @@ class Task(models.Model): defaults = { 'name': msg.get('subject') or _("No Subject"), 'email_from': msg.get('from'), - 'email_cc': msg.get('cc'), 'planned_hours': 0.0, 'partner_id': msg.get('author_id') } diff --git a/addons/test_mail/models/test_mail_models.py b/addons/test_mail/models/test_mail_models.py index 9d59beaf879..1dd93577821 100644 --- a/addons/test_mail/models/test_mail_models.py +++ b/addons/test_mail/models/test_mail_models.py @@ -124,3 +124,10 @@ class MailModel(models.Model): def _value_pc(self): for record in self: record.value_pc = float(record.value) / 100 + + +class MailCC(models.Model): + _name = 'mail.test.cc' + _inherit = ['mail.thread.cc'] + + name = fields.Char() diff --git a/addons/test_mail/security/ir.model.access.csv b/addons/test_mail/security/ir.model.access.csv index 58902f6235f..a856d7a3bac 100644 --- a/addons/test_mail/security/ir.model.access.csv +++ b/addons/test_mail/security/ir.model.access.csv @@ -10,3 +10,6 @@ access_mail_test_full_portal,mail.test.full.portal,model_mail_test_full,base.gro access_mail_test_full_user,mail.test.full.user,model_mail_test_full,base.group_user,1,1,1,1 access_mail_test_portal,mail.test.portal,model_mail_test,base.group_portal,1,1,0,0 access_mail_test_user,mail.test.user,model_mail_test,base.group_user,1,1,1,1 +access_mail_test_cc_portal,mail.test.cc.portal,model_mail_test_cc,base.group_portal,1,0,0,0 +access_mail_test_cc_user,mail.test.cc.user,model_mail_test_cc,base.group_user,1,1,1,1 + diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py index aeddfcd93dd..71c0b6a0a41 100644 --- a/addons/test_mail/tests/__init__.py +++ b/addons/test_mail/tests/__init__.py @@ -6,6 +6,7 @@ from . import test_mail_message from . import test_mail_mail from . import test_mail_race from . import test_mail_resend +from . import test_mail_cc from . import test_mail_channel from . import test_mail_gateway from . import test_mail_template diff --git a/addons/test_mail/tests/test_mail_cc.py b/addons/test_mail/tests/test_mail_cc.py new file mode 100644 index 00000000000..30c4613b02d --- /dev/null +++ b/addons/test_mail/tests/test_mail_cc.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.test_mail.tests import common +from odoo.tests import tagged +from odoo.tools import mute_logger, email_split_and_format +from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE + + +@tagged('cc_test') +class TestMailResend(common.BaseFunctionalTest, common.MockEmails): + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_cc_new(self): + alias = self.env['mail.alias'].create({ + 'alias_name': 'cc_record', + 'alias_user_id': False, + 'alias_model_id': self.env['ir.model']._get('mail.test.cc').id, + 'alias_contact': 'everyone'}) + record = self.format_and_process(MAIL_TEMPLATE, target_model='mail.test.cc', to='cc_record@example.com', + cc='cc1@example.com, cc2@example.com') + cc = email_split_and_format(record.email_cc) + self.assertEqual(sorted(cc), ['cc1@example.com', 'cc2@example.com'], 'cc should contains exactly 2 cc') + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_cc_update_with_old(self): + record = self.env['mail.test.cc'].create({'email_cc': 'cc1 , cc2@example.com'}) + alias = self.env['mail.alias'].create({ + 'alias_name': 'cc_record', + 'alias_user_id': False, + 'alias_model_id': self.env['ir.model']._get('mail.test.cc').id, + 'alias_contact': 'everyone', + 'alias_force_thread_id': record.id}) + + self.format_and_process(MAIL_TEMPLATE, subject='Re: Frogs', target_model='mail.test.cc', + msg_id='1198923581.41972151344608186799.JavaMail.diff1@agrolait.com', + to='cc_record@example.com', + cc='cc2 , cc3@example.com') + cc = email_split_and_format(record.email_cc) + self.assertEqual(sorted(cc), ['cc1 ', 'cc2@example.com', 'cc3@example.com'], 'new cc should have been added on record (unique)') + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_cc_update_no_old(self): + record = self.env['mail.test.cc'].create({}) + alias = self.env['mail.alias'].create({ + 'alias_name': 'cc_record', + 'alias_user_id': False, + 'alias_model_id': self.env['ir.model']._get('mail.test.cc').id, + 'alias_contact': 'everyone', + 'alias_force_thread_id': record.id}) + self.format_and_process(MAIL_TEMPLATE, subject='Re: Frogs', target_model='mail.test.cc', + msg_id='1198923581.41972151344608186799.JavaMail.diff1@agrolait.com', + to='cc_record@example.com', + cc='cc2 , cc3@example.com') + cc = email_split_and_format(record.email_cc) + self.assertEqual(sorted(cc), ['cc2 ', 'cc3@example.com'], 'new cc should have been added on record (unique)') + + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_mail_cc_recipient_suggestion(self): + record = self.env['mail.test.cc'].create({'email_cc': 'cc1@example.com, cc2@example.com, cc3 '}) + suggestions = record.message_get_suggested_recipients()[record.id] + self.assertEqual(sorted(suggestions), [ + (False, 'cc1@example.com', 'CC Email'), + (False, 'cc2@example.com', 'CC Email'), + (False, 'cc3 ', 'CC Email') + ], 'cc should be in suggestions') diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py index 40c9604157f..b6a743d874c 100644 --- a/odoo/tools/mail.py +++ b/odoo/tools/mail.py @@ -474,28 +474,29 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non cr.close() return res -def email_split(text): - """ Return a list of the email addresses found in ``text`` """ +def email_split_tuples(text): + """ Return a list of (name, email) addresse tuples found in ``text``""" if not text: return [] - return [addr[1] for addr in getaddresses([text]) + return [(addr[0], addr[1]) for addr in getaddresses([text]) # getaddresses() returns '' when email parsing fails, and # sometimes returns emails without at least '@'. The '@' # is strictly required in RFC2822's `addr-spec`. if addr[1] if '@' in addr[1]] +def email_split(text): + """ Return a list of the email addresses found in ``text`` """ + if not text: + return [] + return [email for (name, email) in email_split_tuples(text)] + def email_split_and_format(text): """ Return a list of email addresses found in ``text``, formatted using formataddr. """ if not text: return [] - return [formataddr((addr[0], addr[1])) for addr in getaddresses([text]) - # getaddresses() returns '' when email parsing fails, and - # sometimes returns emails without at least '@'. The '@' - # is strictly required in RFC2822's `addr-spec`. - if addr[1] - if '@' in addr[1]] + return [formataddr((name, email)) for (name, email) in email_split_tuples(text)] def email_normalize(text): """ Sanitize and standardize email address entries.