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.