diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index e7427f36e75..aaf41ba89a0 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -1110,11 +1110,15 @@ class Lead(models.Model): return [new_group] + groups - @api.model - def _notify_get_reply_to(self, res_ids, default=None): - leads = self.sudo().browse(res_ids) - aliases = self.env['crm.team']._notify_get_reply_to(leads.mapped('team_id').ids, default=default) - return {lead.id: aliases.get(lead.team_id.id or 0, False) for lead in leads} + @api.multi + def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None): + """ Override to set alias of lead and opportunities to their sales team if any. """ + aliases = self.mapped('team_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None) + res = {lead.id: aliases.get(lead.team_id.id) for lead in self} + leftover = self.filtered(lambda rec: not rec.team_id) + if leftover: + res.update(super(Lead, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names)) + return res @api.multi def get_formview_id(self, access_uid=None): diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py index 17c728b96c7..bcb5b55445e 100644 --- a/addons/hr_recruitment/models/hr_recruitment.py +++ b/addons/hr_recruitment/models/hr_recruitment.py @@ -335,12 +335,15 @@ class Applicant(models.Model): return 'hr_recruitment.mt_applicant_stage_changed' return super(Applicant, self)._track_subtype(init_values) - @api.model - def _notify_get_reply_to(self, ids, default=None): - """ Override to get the reply_to of the parent project. """ - applicants = self.sudo().browse(ids) - aliases = self.env['hr.job']._notify_get_reply_to(applicants.mapped('job_id').ids, default=default) - return dict((applicant.id, aliases.get(applicant.job_id and applicant.job_id.id or 0, False)) for applicant in applicants) + @api.multi + def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None): + """ Override to set alias of applicants to their job definition if any. """ + aliases = self.mapped('job_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None) + res = {app.id: aliases.get(app.job_id.id) for app in self} + leftover = self.filtered(lambda rec: not rec.job_id) + if leftover: + res.update(super(Applicant, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names)) + return res @api.multi def message_get_suggested_recipients(self): diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index f2a2f176a6b..d5b70f08014 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -198,7 +198,6 @@ class Channel(models.Model): @api.multi def _notify_specific_email_values(self, message): - self.ensure_one() res = super(Channel, self)._notify_specific_email_values(message) try: headers = safe_eval(res.get('headers', dict())) diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 4362358b864..45d66dac6a5 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -695,15 +695,10 @@ class Message(models.Model): @api.model def _get_reply_to(self, values): - """ Return a specific reply_to: alias of the document through - _notify_get_reply_to or take the email_from """ + """ Return a specific reply_to for the document """ model, res_id, email_from = values.get('model', self._context.get('default_model')), values.get('res_id', self._context.get('default_res_id')), values.get('email_from') # ctx values / defualt_get res ? - if model and hasattr(self.env[model], '_notify_get_reply_to'): - # return self.env[model].browse(res_id)._notify_get_reply_to([res_id], default=email_from)[res_id] - return self.env[model]._notify_get_reply_to([res_id], default=email_from)[res_id] - else: - # return self.env['mail.thread']._notify_get_reply_to(default=email_from)[None] - return self.env['mail.thread']._notify_get_reply_to([None], default=email_from)[None] + records = self.env[model].browse([res_id]) if model and res_id else None + return self.env['mail.thread']._notify_get_reply_to_on_records(default=email_from, records=records)[res_id or False] @api.model def _get_message_id(self, values): diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index f92b8006fc3..2ce459cce4e 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -665,14 +665,22 @@ class MailThread(models.AbstractModel): Each action is a dict containing url and title of the button. Groups has a default value that you can find in mail_thread - _notify_classify_recipients method. + ``_notify_classify_recipients`` method. """ return groups @api.multi def _notify_classify_recipients(self, message, recipients): - # At this point, all access rights should be ok. We sudo everything to - # access rights checks and speedup the computation. + """ Classify recipients to be notified of a message in groups to have + specific rendering depending on their group. For example users could + have access to buttons customers should not have in their emails. + + Module-specific grouping should be done by overriding ``_notify_get_groups`` + method defined here-under. + + :param message: mail.message record about to be notified + :param recipients: res.partner recordset to notify + """ result = {} access_link = self._notify_get_action_link('view') @@ -714,56 +722,115 @@ class MailThread(models.AbstractModel): return result - @api.model - def _notify_get_reply_to(self, res_ids, default=None): - """ Returns the preferred reply-to email address that is basically the - alias of the document, if it exists. Override this method to implement - a custom behavior about reply-to for generated emails. """ - model_name = self.env.context.get('thread_model') or self._name - alias_domain = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.domain") - res = dict.fromkeys(res_ids, False) + def _notify_classify_recipients_on_records(self, message, recipients, records=None): + """ Generic wrapper on ``_notify_classify_recipients`` checking mail.thread + inheritance and allowing to call model-specific implementation in a one liner. + This method should not be overridden. """ + if records and hasattr(records, '_notify_classify_recipients'): + return records._notify_classify_recipients(message, recipients) + return self._notify_classify_recipients(message, recipients) + + @api.multi + def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None): + """ Returns the preferred reply-to email address when replying to a thread + on documents. Documents are either given by self it this method is called + as a true multi method on a record set or can be given using records to + have a generic implementation available for all models. + + Reply-to is formatted like "MyCompany MyDocument ". + Heuristic it the following: + + * search for specific aliases as they always have priority; it is limited + to aliases linked to documents (like project alias for task for example); + * use catchall address; + * use default; + + This method works on documents + + * as a true multi method for models inheriting from mail.thread; call + ``records._notify_get_reply_to(...)``; + * as a generic implementation if records are given; call ``MailThread. + _notify_get_reply_to(records=records, ...)``; + * as a generic implementation is self is a void mail.thread record set + meaning catchall is computed; call ``MailThread._notify_get_reply_to + (records=None)``; + + Tweak this method on a specific model by overriding if it inherits from + mail.thread. An example would be tasks taking their reply-to alias from + their project. + + :param default: default email if no alias or catchall is found; + :param records: record_set if self if a generic mail.thread record allowing + generic implementation of finding reply-to; + :param company: used to compute company name part of the from name; provide + it if already known, otherwise fall back on user company; + :param doc_names: dict(res_id, doc_name) used to compute doc name part of + the from name; provide it if already known to avoid queries, otherwise + name_get on document will be performed; + + :return result: dictionary. Keys are record IDs and value is formatted + like an email "Company_name Document_name "/ + """ + _records = self if self and self._name != 'mail.thread' else records + model = _records._name if _records and _records._name != 'mail.thread' else False + res_ids = _records.ids if _records and model else [] + _res_ids = res_ids or [False] # always have a default value located in False + + alias_domain = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.domain") + result = dict.fromkeys(_res_ids, False) + result_email = dict() + doc_names = doc_names if doc_names else dict() - # alias domain: check for aliases and catchall - aliases = {} - doc_names = {} if alias_domain: - if model_name and model_name != 'mail.thread' and res_ids: + if model and res_ids: + if not doc_names: + doc_names = dict((rec.id, rec.display_name) for rec in _records) + mail_aliases = self.env['mail.alias'].sudo().search([ - ('alias_parent_model_id.model', '=', model_name), + ('alias_parent_model_id.model', '=', model), ('alias_parent_thread_id', 'in', res_ids), ('alias_name', '!=', False)]) - # take only first found alias for each thread_id, to match - # order (1 found -> limit=1 for each res_id) + # take only first found alias for each thread_id, to match order (1 found -> limit=1 for each res_id) for alias in mail_aliases: - if alias.alias_parent_thread_id not in aliases: - aliases[alias.alias_parent_thread_id] = '%s@%s' % (alias.alias_name, alias_domain) - doc_names.update( - dict((ng_res[0], ng_res[1]) - for ng_res in self.env[model_name].sudo().browse(aliases).name_get())) + result_email.setdefault(alias.alias_parent_thread_id, '%s@%s' % (alias.alias_name, alias_domain)) + # left ids: use catchall - left_ids = set(res_ids).difference(set(aliases)) + left_ids = set(_res_ids) - set(result_email) if left_ids: - catchall_alias = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.alias") - if catchall_alias: - aliases.update(dict((res_id, '%s@%s' % (catchall_alias, alias_domain)) for res_id in left_ids)) - # compute name of reply-to - company_name = self.env.user.company_id.name - for res_id in aliases: - email_name = '%s%s' % (company_name, doc_names.get(res_id) and (' ' + doc_names[res_id]) or '') - email_addr = aliases[res_id] - res[res_id] = formataddr((email_name, email_addr)) - left_ids = set(res_ids).difference(set(aliases)) + catchall = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.alias") + if catchall: + result_email.update(dict((rid, '%s@%s' % (catchall, alias_domain)) for rid in left_ids)) + + # compute name of reply-to - TDE tocheck: quotes and stuff like that + company_name = company.name if company else self.env.user.company_id.name + for res_id in result_email.keys(): + name = '%s%s%s' % (company_name, ' ' if doc_names.get(res_id) else '', doc_names.get(res_id, '')) + result[res_id] = formataddr((name, result_email[res_id])) + + left_ids = set(_res_ids) - set(result_email) if left_ids: - res.update(dict((res_id, default) for res_id in res_ids)) - return res + result.update(dict((res_id, default) for res_id in left_ids)) + + return result + + @api.model + def _notify_get_reply_to_on_records(self, default=None, records=None, company=None, doc_names=None): + """ Generic wrapper on ``_notify_get_reply_to`` checking mail.thread inheritance + and allowing to call model-specific implementation in a one liner. This + method should not be overridden. """ + if records and hasattr(records, '_notify_get_reply_to'): + return records._notify_get_reply_to(default=default, company=company, doc_names=doc_names) + return self._notify_get_reply_to(default=default, records=records, company=company, doc_names=doc_names) @api.multi def _notify_specific_email_values(self, message): """ Get specific notification email values to store on the notification mail.mail. Override to add values related to a specific model. - :param MailMessage message: mail.message record being notified by email + :param message: mail.message record being notified by email """ + if not self: + return {} self.ensure_one() database_uuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid') return {'headers': repr({ @@ -771,6 +838,15 @@ class MailThread(models.AbstractModel): 'X-Odoo-db-uuid': database_uuid })} + @api.model + def _notify_specific_email_values_on_records(self, message, records=None): + """ Generic wrapper on ``_notify_specific_email_values`` checking mail.thread + inheritance and allowing to call model-specific implementation in a one liner. + This method should not be overridden. """ + if records and hasattr(records, '_notify_specific_email_values'): + return records._notify_specific_email_values(message) + return self._notify_specific_email_values(message) + @api.multi def _notify_email_recipients(self, message, recipient_ids): """ Format email notification recipient values to store on the notification @@ -778,13 +854,22 @@ class MailThread(models.AbstractModel): recipients. Override to generate other mail values like email_to or email_cc. - :param MailMessage message: mail.message record being notified by email - :param list recipient_ids: list of res.partner ids to notify + :param message: mail.message record being notified by email + :param recipient_ids: res.partner recordset to notify """ return { 'recipient_ids': [(4, pid) for pid in recipient_ids] } + @api.model + def _notify_email_recipients_on_records(self, message, recipient_ids, records=None): + """ Generic wrapper on ``_notify_email_recipients`` checking mail.thread + inheritance and allowing to call model-specific implementation in a one liner. + This method should not be overridden. """ + if records and hasattr(records, '_notify_email_recipients'): + return records._notify_email_recipients(message, recipient_ids) + return self._notify_email_recipients(message, recipient_ids) + # ------------------------------------------------------ # Mail gateway # ------------------------------------------------------ @@ -1977,18 +2062,19 @@ class MailThread(models.AbstractModel): author = self.env.user.partner_id if not author.email: raise exceptions.UserError(_("Unable to notify message, please configure the sender's email address.")) + email_from = formataddr((author.name, author.email)) msg_values = { 'subject': subject, 'body': body, 'author_id': author.id, - 'email_from': formataddr((author.name, author.email)), + 'email_from': email_from, 'message_type': 'notification', 'partner_ids': partner_ids, 'model': False, 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'), 'record_name': False, - 'reply_to': self.env['mail.thread'].sudo()._notify_get_reply_to([0])[0], + 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from, records=None)[False], 'message_id': tools.generate_tracking_message_id('message-notify'), } msg_values.update(kwargs) @@ -2011,18 +2097,19 @@ class MailThread(models.AbstractModel): author = self.env.user.partner_id if not author.email: raise exceptions.UserError(_("Unable to log message, please configure the sender's email address.")) + email_from = formataddr((author.name, author.email)) message_values = { 'subject': subject, 'body': body, 'author_id': author.id, - 'email_from': formataddr((author.name, author.email)), + 'email_from': email_from, 'message_type': message_type, 'model': kwargs.get('model', self._name), 'res_id': self.ids[0] if self.ids else False, 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'), 'record_name': False, - 'reply_to': self.env['mail.thread'].sudo()._notify_get_reply_to([0])[0], + 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from, records=None)[False], 'message_id': tools.generate_tracking_message_id('message-notify'), } message_values.update(kwargs) diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 09554230025..f6472eafd0b 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -100,8 +100,8 @@ class Partner(models.Model): # custom values custom_values = dict() - if message.res_id and message.model in self.env and hasattr(self.env[message.model], '_notify_specific_email_values'): - custom_values = self.env[message.model].browse(message.res_id)._notify_specific_email_values(message) + if message.res_id and message.model: + custom_values = self.env['mail.thread']._notify_specific_email_values_on_records(message, records=self.env[message.model].browse(message.res_id)) mail_values = { 'mail_message_id': message.id, @@ -122,11 +122,8 @@ class Partner(models.Model): # cache so should not impact performances. mail_message_id = mail_values.get('mail_message_id') message = self.env['mail.message'].browse(mail_message_id) if mail_message_id else None - if message and message.model and message.res_id and message.model in self.env and hasattr(self.env[message.model], '_notify_email_recipients'): - tig = self.env[message.model].browse(message.res_id) - recipient_values = tig._notify_email_recipients(message, email_chunk) - else: - recipient_values = self.env['mail.thread']._notify_email_recipients(message, email_chunk) + tig = self.env[message.model].browse(message.res_id) if message and message.model and message.res_id else False + recipient_values = self.env['mail.thread']._notify_email_recipients_on_records(message, email_chunk, records=tig) create_values = { 'body_html': body, 'subject': subject, @@ -178,10 +175,8 @@ class Partner(models.Model): base_mail_values = self._notify_prepare_email_values(message, values) # classify recipients: actions / no action - if message.model and message.res_id and hasattr(self.env[message.model], '_notify_classify_recipients'): - recipients = self.env[message.model].browse(message.res_id)._notify_classify_recipients(message, self) - else: - recipients = self.env['mail.thread']._notify_classify_recipients(message, self) + tig = self.env[message.model].browse(message.res_id) if message.model and message.res_id else False + recipients = self.env['mail.thread']._notify_classify_recipients_on_records(message, self, records=tig) emails = self.env['mail.mail'] recipients_nbr, recipients_max = 0, 50 diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 4cc06574ec4..91718f21f8a 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -278,8 +278,8 @@ class MailComposer(models.TransientModel): # compute alias-based reply-to in batch reply_to_value = dict.fromkeys(res_ids, None) if mass_mail_mode and not self.no_auto_thread: - # reply_to_value = self.env['mail.thread'].with_context(thread_model=self.model).browse(res_ids)._notify_get_reply_to(default=self.email_from) - reply_to_value = self.env['mail.thread'].with_context(thread_model=self.model)._notify_get_reply_to(res_ids, default=self.email_from) + records = self.env[self.model].browse(res_ids) + reply_to_value = self.env['mail.thread']._notify_get_reply_to_on_records(default=self.email_from, records=records) for res_id in res_ids: # static wizard (mail.message) values @@ -299,8 +299,7 @@ class MailComposer(models.TransientModel): # mass mailing: rendering override wizard static values if mass_mail_mode and self.model: - if self.model in self.env and hasattr(self.env[self.model], '_notify_specific_email_values'): - mail_values.update(self.env[self.model].browse(res_id)._notify_specific_email_values(False)) + mail_values.update(self.env['mail.thread']._notify_specific_email_values_on_records(False, records=self.env[self.model].browse(res_id))) # keep a copy unless specifically requested, reset record name (avoid browsing records) mail_values.update(notification=not self.auto_delete_message, model=self.model, res_id=res_id, record_name=False) # auto deletion of mail_mail diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 9c9871a7e89..c9bc1b750f2 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -862,13 +862,15 @@ class Task(models.Model): return groups - @api.model - def _notify_get_reply_to(self, res_ids, default=None): - """ Override to get the reply_to of the parent project. """ - tasks = self.sudo().browse(res_ids) - project_ids = tasks.mapped('project_id').ids - aliases = self.env['project.project']._notify_get_reply_to(project_ids, default=default) - return {task.id: aliases.get(task.project_id.id, False) for task in tasks} + @api.multi + def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None): + """ Override to set alias of tasks to their project if any. """ + aliases = self.mapped('project_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None) + res = {task.id: aliases.get(task.project_id.id) for task in self} + leftover = self.filtered(lambda rec: not rec.project_id) + if leftover: + res.update(super(Task, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names)) + return res @api.multi def email_split(self, msg): diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index adbe2213e66..b850fdd9227 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -385,7 +385,7 @@ class TestHeavyMailPerformance(TransactionCase): self.umbrella.message_subscribe(self.user_portal.partner_id.ids) record = self.umbrella.sudo(self.env.user) - with self.assertQueryCount(admin=97, emp=119): # com runbot 90 - 112 // test_mail only: 97 - 119 + with self.assertQueryCount(admin=97, emp=120): # com runbot 90 - 113 // test_mail only: 97 - 120 record.message_post( body='

Test Post Performances

', message_type='comment', diff --git a/addons/website_mail_channel/models/mail_channel.py b/addons/website_mail_channel/models/mail_channel.py index 49d9139ec69..108127b2bb4 100644 --- a/addons/website_mail_channel/models/mail_channel.py +++ b/addons/website_mail_channel/models/mail_channel.py @@ -15,7 +15,6 @@ class MailGroup(models.Model): @api.multi def _notify_specific_email_values(self, message): - self.ensure_one() res = super(MailGroup, self)._notify_specific_email_values(message) try: headers = safe_eval(res.get('headers', dict()))