From 4775bd93a2bf9947236f4f7f06d6f8cb7734f7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 2 Sep 2022 13:28:56 +0000 Subject: [PATCH] [REF] mail: cleanup post with {view, template} wrappers RATIONALE Purpose of this commit is to cleanup main post helpers and have a more easy and understandable way of calling them. SUMMARY We now have two main API methods, based on business flow: either posting on documents, either sending a mass mailing. Indeed those two flows are different * post: create message, then launch notification process by taking into account subtype, followers, ... * mail: create mails in batch with recipients being based on template or given partners. No notifications is involved, only maybe traces if a mass mailing is linked Delegate QWeb rendering to the render mixin (i.e. _render_template_qweb_view) in order to have a single point to forge evaluation context and re-use existing rendering code. SPECIFICATIONS Main API helpers are now * ``message_post_with_source``: (batch) post on records, using an ir.ui.view (given a record or its xml id) or a mail.template record (given a record or its xml id). When using a template, a composer is called to post on each record (as batch post is not yet supported). When using a view, a direct call to message_post using the rendered bodies is done, one record at a time. * ``message_mail_with_source``: send a mass mailing on records, acting like invoking the mail composer in mass mode. Same arguments are valid, either a reference to a view, either a reference to a mail template. Other helpers are * ``_message_log_with_view``: (batch) log on records, using an ir.ui.view to render the body using QWeb (no notification process); * ``_message_log(_batch)``: (batch) log on records (no notification process); * ``message_notify``: notify partners on records (creating notifications specifically for some people while message itself is not displayed in chatter); Code migration * ``message_post_with_template`` in "mass mode": use ``message_mail_with_source`` and set the template record as source; * ``message_post_with_template`` in "comment" mode: use ``message_post_with_source`` and set the template record as source; * ``message_post_with_view``: its main usage was to post on a document, in which case it generally can be replaced by ``message_mail_with_source`` using the view reference as source; Task-2710804 (Mail: Clean MailThread Posting API) Part-of: odoo/odoo#99482 --- addons/calendar/models/calendar_event.py | 4 +- addons/calendar/views/calendar_views.xml | 2 +- addons/crm/models/crm_lead.py | 4 +- addons/crm_iap_enrich/models/crm_lead.py | 10 +- .../models/crm_iap_lead_mining_request.py | 4 +- addons/event_booth/models/event_booth.py | 4 +- .../event_sale/models/event_registration.py | 4 +- addons/hr_expense/models/hr_expense.py | 15 +- addons/hr_recruitment/models/hr_employee.py | 4 +- .../wizard/applicant_refuse_reason.py | 19 +- .../models/hr_contract.py | 4 +- addons/mail/models/ir_actions_server.py | 10 +- addons/mail/models/mail_activity.py | 8 +- addons/mail/models/mail_activity_mixin.py | 10 +- addons/mail/models/mail_thread.py | 399 +++++++++++++----- addons/mail_group/models/mail_group.py | 2 +- addons/mail_plugin/controllers/mail_plugin.py | 8 +- addons/mass_mailing/models/mail_thread.py | 12 +- addons/mrp/models/stock_rule.py | 8 +- .../models/res_company.py | 4 +- .../models/res_partner.py | 4 +- addons/portal/wizard/portal_share.py | 16 +- .../models/account_move.py | 5 +- addons/purchase/models/purchase.py | 12 +- .../purchase_requisition/models/purchase.py | 8 +- addons/purchase_stock/models/purchase.py | 5 +- addons/rating/models/rating_mixin.py | 11 +- addons/sale/models/payment_transaction.py | 14 +- addons/sale/models/sale_order.py | 8 +- addons/sale/tests/test_controllers.py | 4 +- addons/sale/tests/test_sale_order.py | 8 +- addons/sale/tests/test_sale_to_invoice.py | 4 +- .../sale/wizard/sale_make_invoice_advance.py | 4 +- addons/sale_stock/models/stock.py | 4 +- .../tests/test_sale_stock_margin.py | 5 +- addons/stock/models/stock_move_line.py | 4 +- addons/stock/models/stock_picking.py | 6 +- addons/stock/wizard/stock_picking_return.py | 4 +- addons/test_mail/tests/test_message_post.py | 98 +++-- addons/test_mail/tests/test_message_track.py | 2 +- addons/test_mail/tests/test_performance.py | 13 +- addons/website_blog/models/website_blog.py | 4 +- .../models/crm_reveal_rule.py | 4 +- .../website_event_track/models/event_track.py | 4 +- addons/website_forum/models/forum.py | 10 +- 45 files changed, 509 insertions(+), 287 deletions(-) diff --git a/addons/calendar/models/calendar_event.py b/addons/calendar/models/calendar_event.py index 43bb858577b..ca3da46a725 100644 --- a/addons/calendar/models/calendar_event.py +++ b/addons/calendar/models/calendar_event.py @@ -750,9 +750,9 @@ class Meeting(models.Model): raise UserError(_("There are no attendees on these events")) template_id = self.env['ir.model.data']._xmlid_to_res_id('calendar.calendar_template_meeting_update', raise_if_not_found=False) # The mail is sent with datetime corresponding to the sending user TZ - composition_mode = self.env.context.get('composition_mode', 'comment') + default_composition_mode = self.env.context.get('default_composition_mode', self.env.context.get('composition_mode', 'comment')) compose_ctx = dict( - default_composition_mode=composition_mode, + default_composition_mode=default_composition_mode, default_model='calendar.event', default_res_ids=self.ids, default_use_template=bool(template_id), diff --git a/addons/calendar/views/calendar_views.xml b/addons/calendar/views/calendar_views.xml index d8a21fe6d35..8049ce2d67b 100644 --- a/addons/calendar/views/calendar_views.xml +++ b/addons/calendar/views/calendar_views.xml @@ -73,7 +73,7 @@
-
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 0442fe57ed5..85468e3973d 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -1561,9 +1561,9 @@ class Lead(models.Model): def _merge_log_summary(self, merged_followers, opportunities_tail): """Log the merge message on the lead.""" self.ensure_one() - self.message_post_with_view( + self.message_post_with_source( "crm.crm_lead_merge_summary", - values={ + render_values={ "merged_followers": merged_followers, "opportunities": opportunities_tail, "is_html_empty": is_html_empty, diff --git a/addons/crm_iap_enrich/models/crm_lead.py b/addons/crm_iap_enrich/models/crm_lead.py index 6732e2ee57e..8c65cd6beba 100644 --- a/addons/crm_iap_enrich/models/crm_lead.py +++ b/addons/crm_iap_enrich/models/crm_lead.py @@ -68,7 +68,7 @@ class Lead(models.Model): normalized_email = tools.email_normalize(lead.email_from) if not normalized_email: - lead.message_post_with_view( + lead.message_post_with_source( 'crm_iap_enrich.mail_message_lead_enrich_no_email', subtype_xmlid='mail.mt_note', ) @@ -78,7 +78,7 @@ class Lead(models.Model): # Discard domains of generic email providers as it won't return relevant information if email_domain in iap_tools._MAIL_DOMAIN_BLACKLIST: lead.write({'iap_enrich_done': True}) - lead.message_post_with_view( + lead.message_post_with_source( 'crm_iap_enrich.mail_message_lead_enrich_notfound', subtype_xmlid='mail.mt_note', ) @@ -128,7 +128,7 @@ class Lead(models.Model): iap_data = iap_response.get(str(lead.id)) if not iap_data: lead.write({'iap_enrich_done': True}) - lead.message_post_with_view( + lead.message_post_with_source( 'crm_iap_enrich.mail_message_lead_enrich_notfound', subtype_xmlid='mail.mt_note', ) @@ -161,9 +161,9 @@ class Lead(models.Model): template_values = iap_data template_values['flavor_text'] = _("Lead enriched based on email address") - lead.message_post_with_view( + lead.message_post_with_source( 'iap_mail.enrich_company', - values=template_values, + render_values=template_values, subtype_xmlid='mail.mt_note', ) diff --git a/addons/crm_iap_mine/models/crm_iap_lead_mining_request.py b/addons/crm_iap_mine/models/crm_iap_lead_mining_request.py index cb9ef3c4e66..615ea7c347a 100644 --- a/addons/crm_iap_mine/models/crm_iap_lead_mining_request.py +++ b/addons/crm_iap_mine/models/crm_iap_lead_mining_request.py @@ -278,9 +278,9 @@ class CRMLeadMiningRequest(models.Model): leads = self.env['crm.lead'].create(lead_vals_list) for lead in leads: if messages_to_post.get(lead.reveal_id): - lead.message_post_with_view( + lead.message_post_with_source( 'iap_mail.enrich_company', - values=messages_to_post[lead.reveal_id], + render_values=messages_to_post[lead.reveal_id], subtype_xmlid='mail.mt_note', ) diff --git a/addons/event_booth/models/event_booth.py b/addons/event_booth/models/event_booth.py index 474ea92c545..592d7223f60 100644 --- a/addons/event_booth/models/event_booth.py +++ b/addons/event_booth/models/event_booth.py @@ -100,9 +100,9 @@ class EventBooth(models.Model): def _post_confirmation_message(self): for booth in self: - booth.event_id.message_post_with_view( + booth.event_id.message_post_with_source( 'event_booth.event_booth_booked_template', - values={ + render_values={ 'booth': booth, }, subtype_xmlid='event_booth.mt_event_booth_booked', diff --git a/addons/event_sale/models/event_registration.py b/addons/event_sale/models/event_registration.py index 95c0f47ec46..521f931d5ce 100644 --- a/addons/event_sale/models/event_registration.py +++ b/addons/event_sale/models/event_registration.py @@ -77,9 +77,9 @@ class EventRegistration(models.Model): registrations = super(EventRegistration, self).create(vals_list) for registration in registrations: if registration.sale_order_id: - registration.message_post_with_view( + registration.message_post_with_source( 'mail.message_origin_link', - values={'self': registration, 'origin': registration.sale_order_id}, + render_values={'self': registration, 'origin': registration.sale_order_id}, subtype_xmlid='mail.mt_note', ) return registrations diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 6fd2566fdd8..85204ffd4e8 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -636,11 +636,12 @@ class HrExpense(models.Model): def refuse_expense(self, reason): self.write({'is_refused': True}) self.sheet_id.write({'state': 'cancel'}) - self.sheet_id.message_post_with_view( - 'hr_expense.hr_expense_template_refuse_reason', - values={'reason': reason, 'is_sheet': False, 'name': self.name}, - subtype_xmlid='mail.mt_comment', - ) + for expense in self: + expense.sheet_id.message_post_with_source( + 'hr_expense.hr_expense_template_refuse_reason', + render_values={'reason': reason, 'is_sheet': False, 'name': expense.name}, + subtype_xmlid='mail.mt_comment', + ) @api.model def get_expense_dashboard(self): @@ -1218,10 +1219,10 @@ class HrExpenseSheet(models.Model): self.write({'state': 'cancel'}) subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment') for sheet in self: - sheet.message_post_with_view( + sheet.message_post_with_source( 'hr_expense.hr_expense_template_refuse_reason', subtype_id=subtype_id, - values={'reason': reason, 'is_sheet': True, 'name': sheet.name}, + render_values={'reason': reason, 'is_sheet': True, 'name': sheet.name}, ) self.activity_update() diff --git a/addons/hr_recruitment/models/hr_employee.py b/addons/hr_recruitment/models/hr_employee.py index 4013d5ceebc..2ea3ad7af52 100644 --- a/addons/hr_recruitment/models/hr_employee.py +++ b/addons/hr_recruitment/models/hr_employee.py @@ -27,9 +27,9 @@ class HrEmployee(models.Model): employees = super().create(vals_list) for employee in employees: if employee.applicant_id: - employee.applicant_id.message_post_with_view( + employee.applicant_id.message_post_with_source( 'hr_recruitment.applicant_hired_template', - values={'applicant': employee.applicant_id}, + render_values={'applicant': employee.applicant_id}, subtype_xmlid='hr_recruitment.mt_applicant_hired', ) return employees diff --git a/addons/hr_recruitment/wizard/applicant_refuse_reason.py b/addons/hr_recruitment/wizard/applicant_refuse_reason.py index d44413b3d94..b7aea323786 100644 --- a/addons/hr_recruitment/wizard/applicant_refuse_reason.py +++ b/addons/hr_recruitment/wizard/applicant_refuse_reason.py @@ -45,9 +45,16 @@ class ApplicantGetRefuseReason(models.TransientModel): self.applicant_ids.write({'refuse_reason_id': self.refuse_reason_id.id, 'active': False}) if self.send_mail: applicants = self.applicant_ids.filtered(lambda x: x.email_from or x.partner_id.email) - applicants.with_context(active_test=True).message_post_with_template( - self.template_id.id, - auto_delete_message=True, - email_layout_xmlid='mail.mail_notification_light', - subtype_xmlid='mail.mt_note', - ) + # TDE note: keeping 16.0 behavior, clean me please + message_values = { + 'auto_delete_message' : True, + 'email_layout_xmlid' : 'mail.mail_notification_light', + } + if len(applicants) > 1: + applicants.with_context(active_test=True).message_mail_with_source(self.template_id, **message_values) + else: + applicants.with_context(active_test=True).message_post_with_source( + self.template_id, + subtype_xmlid='mail.mt_note', + **message_values + ) diff --git a/addons/hr_work_entry_holidays/models/hr_contract.py b/addons/hr_work_entry_holidays/models/hr_contract.py index 2b608ce38df..7410225015e 100644 --- a/addons/hr_work_entry_holidays/models/hr_contract.py +++ b/addons/hr_work_entry_holidays/models/hr_contract.py @@ -134,9 +134,9 @@ class HrContract(models.Model): ).create(all_new_leave_vals) new_leaves.filtered(lambda l: l.state in 'validate')._validate_leave_request() for index, new_leave in enumerate(new_leaves): - new_leave.message_post_with_view( + new_leave.message_post_with_source( 'mail.message_origin_link', - values={'self': new_leave, 'origin': all_new_leave_origin[index]}, + render_values={'self': new_leave, 'origin': all_new_leave_origin[index]}, subtype_xmlid='mail.mt_note', ) return super(HrContract, self - specific_contracts).write(vals) diff --git a/addons/mail/models/ir_actions_server.py b/addons/mail/models/ir_actions_server.py index 87a7e24f651..2192e3eb34b 100644 --- a/addons/mail/models/ir_actions_server.py +++ b/addons/mail/models/ir_actions_server.py @@ -207,12 +207,10 @@ class ServerActions(models.Model): subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment') else: subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note') - for record in records: - record.message_post_with_template( - self.template_id.id, - composition_mode='comment', - subtype_id=subtype_id, - ) + records.message_post_with_source( + self.template_id, + subtype_id=subtype_id, + ) else: template = self.template_id.with_context(cleaned_ctx) for res_id in res_ids: diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 593c170c10e..824a0a1179b 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -574,16 +574,16 @@ class MailActivity(models.Model): next_activities_values.append(vals) # post message on activity, before deleting it - activity_message = record.message_post_with_view( + activity_message = record.message_post_with_source( 'mail.message_activity_done', - values={ + attachment_ids=attachment_ids, + render_values={ 'activity': activity, 'feedback': feedback, 'display_assignee': activity.user_id != self.env.user }, - subtype_xmlid='mail.mt_activities', mail_activity_type_id=activity.activity_type_id.id, - attachment_ids=[Command.link(attachment_id) for attachment_id in attachment_ids] if attachment_ids else [], + subtype_xmlid='mail.mt_activities', ) # Moving the attachments in the message diff --git a/addons/mail/models/mail_activity_mixin.py b/addons/mail/models/mail_activity_mixin.py index 3d5fcb94e46..9cc3c396fc7 100644 --- a/addons/mail/models/mail_activity_mixin.py +++ b/addons/mail/models/mail_activity_mixin.py @@ -342,12 +342,10 @@ class MailActivityMixin(models.AbstractModel): template = self.env['mail.template'].browse(template_id).exists() if not template: return False - for record in self: - record.message_post_with_template( - template_id, - composition_mode='comment', - subtype_xmlid='mail.mt_comment', - ) + self.message_post_with_source( + template, + subtype_xmlid='mail.mt_comment', + ) return True def activity_search(self, act_type_xmlids='', user_id=None, additional_domain=None): diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 5cc47b65136..d9b0498e65f 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -635,7 +635,7 @@ class MailThread(models.AbstractModel): :param dict changes: mapping {record_id: (changed_field_names, tracking_value_ids)} containing existing records only """ - if not changes: + if not self or not changes: return True # Clean the context to get rid of residual default_* keys # that could cause issues afterward during the mail.message @@ -644,15 +644,18 @@ class MailThread(models.AbstractModel): # its creation, but could refer to wrong parent message id, # leading to a traceback in case the related message_id # doesn't exist - self = self.with_context(clean_context(self._context)) + cleaned_self = self.with_context(clean_context(self._context))._fallback_lang() templates = self._track_template(changes) + default_composition_mode = 'mass_mail' if len(self) != 1 else 'comment' for _field_name, (template, post_kwargs) in templates.items(): if not template: continue - if isinstance(template, str): - self._fallback_lang().message_post_with_view(template, **post_kwargs) + + composition_mode = post_kwargs.pop('composition_mode', default_composition_mode) + if composition_mode == 'mass_mail': + cleaned_self.message_mail_with_source(template, **post_kwargs) else: - self._fallback_lang().message_post_with_template(template.id, **post_kwargs) + cleaned_self.message_post_with_source(template, **post_kwargs) return True def _track_template(self, changes): @@ -1813,6 +1816,8 @@ class MailThread(models.AbstractModel): :param int author_id: optional ID of partner record being the author. See ``_message_compute_author`` that uses it to make email_from / author_id coherent; :param int parent_id: handle thread formation + :param str subtype_xmlid: optional xml id of a mail.message.subtype to + fetch, will force value of subtype_id; :param int subtype_id: subtype_id of the message, used mainly for followers notification mechanism; :param list(int) partner_ids: partner_ids to notify in addition to partners @@ -1909,9 +1914,9 @@ class MailThread(models.AbstractModel): 'res_id': self.id, # content 'body': body, - 'subject': subject or False, 'message_type': message_type, 'parent_id': self._message_compute_parent_id(parent_id), + 'subject': subject or False, 'subtype_id': subtype_id, # recipients 'partner_ids': partner_ids, @@ -2094,95 +2099,203 @@ class MailThread(models.AbstractModel): # MESSAGE POST API / WRAPPERS # ------------------------------------------------------------ - def _message_compose_with_view(self, views_or_xmlid, message_log=False, **kwargs): - """ Helper method to send a mail / post a message / log a note using - a view_id to render using the ir.qweb engine. This method is stand - alone, because there is nothing in template and composer that allows - to handle views in batch. This method should probably disappear when - templates handle ir ui views. """ - values = kwargs.pop('values', None) or dict() - try: - from odoo.addons.http_routing.models.ir_http import slug - values['slug'] = slug - except ImportError: - values['slug'] = lambda self: self.id - view_ref = views_or_xmlid.id if isinstance(views_or_xmlid, models.BaseModel) else views_or_xmlid + def message_mail_with_source(self, source_ref, render_values=None, + message_type='notification', + auto_commit=False, + **kwargs): + """ Send a mass mail on self, using an external source to render part + of the content. It can be either a 'mail.template', either a view used + to render the body using QWeb. - messages_as_sudo = self.env['mail.message'] - for record in self: - values['object'] = record - rendered_template = self.env['ir.qweb']._render(view_ref, values, minimal_qcontext=True, raise_if_not_found=False) - if not rendered_template: - continue - if message_log: - messages_as_sudo += record._message_log(body=rendered_template, **kwargs) - else: - kwargs['body'] = rendered_template - # a bit complicated to handle, to be improved soon in master - _mails_as_sudo, _messages_as_sudo = record.message_post_with_template(False, **kwargs) - if _messages_as_sudo: - messages_as_sudo += _messages_as_sudo + SPOILER: this method currently calls a composer in a loop when using + a view even if it is suboptimal. This is due to current composer + implementation.. This will be cleaned soon to optimize mass mailing + through mail.thread and lessen usage of composer itself. - return messages_as_sudo + Default values + * subtype_id: will be False, forced by composer in mass mode; - def message_post_with_view(self, views_or_xmlid, **kwargs): - """ Helper method to send a mail / post a message using a view_id """ - return self._message_compose_with_view(views_or_xmlid, **kwargs) + :param record/str source_ref: reference to a source for rendering. + It can be one of + * a MailTemplate record. It will be used to render the various + message values (body, subject, recipients, ...). It should behave + like using the mail composer with a template; + * an IrUIView record. It will be used to render the content + (body). Other fields are left to the caller and/or default values + computation; + * an XmlID of a MailTemplate or of an IrUiView: see above; + :param dict render_values: additional rendering values for qweb context; - def message_post_with_template(self, template_id, email_layout_xmlid=None, auto_commit=False, **kwargs): - """ Helper method to send a mail with a template - :param template_id : the id of the template to render to create the body of the message - :param **kwargs : parameter to create a mail.compose.message woaerd (which inherit from mail.message) + :param str message_type: one of 'notification' or 'comment'; + :param bool auto_commit: auto commit after each batch of emails sent + (see ``MailComposer._action_send_mail()``); + :param dict kwargs: additional values given to the 'mail.compose.message' + creation; + + :return: created mail.mail records, as sudo """ + template, view = self._get_source_from_ref(source_ref) + # preliminary value safety check self._raise_for_invalid_parameters( set(kwargs.keys()), - forbidden_names={'model', 'res_id'} + forbidden_names={'body', 'composition_mode', 'model', 'res_id', 'values'} ) - # Get composition mode, or force it according to the number of record in self - if not kwargs.get('composition_mode'): - kwargs['composition_mode'] = 'comment' if len(self.ids) == 1 else 'mass_mail' - if not kwargs.get('message_type'): - kwargs['message_type'] = 'notification' - res_id = self.ids[0] if self.ids else 0 - res_ids = self.ids + # with a view, render bodies in batch (template is managed by composer) + bodies = self.env['mail.render.mixin']._render_template_qweb_view( + view, + self._name, + self.ids, + add_context=render_values, + ) if view else {} - # support xml based subtype id - if kwargs.get('subtype_xmlid') and not kwargs.get('subtype_id'): - kwargs['subtype_id'] = self.env['ir.model.data']._xmlid_to_res_id(kwargs.pop('subtype_xmlid')) + # Prepare composer values for creation + composer_values = { + 'composition_mode': 'mass_mail', + 'message_type': message_type, + # subtype is not really used in mass mail mode as it is used mainly + # when posting, but keep it in case it is used in post send + 'subtype_id': kwargs.pop('subtype_id', False) or self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'), + **kwargs, + } + composer_ctx = { + 'default_composition_mode': 'mass_mail', + 'default_model': self._name, + 'default_template_id': template.id if template else False, + } - # Create the composer - composer = self.env['mail.compose.message'].with_context( - active_id=res_id, - active_ids=res_ids, - active_model=self._name, - default_composition_mode=kwargs['composition_mode'], - default_email_layout_xmlid=email_layout_xmlid, - default_model=self._name, - default_res_id=res_id, - default_template_id=template_id, - ).create(kwargs) - # Simulate the onchange (like trigger in form the view) only - # when having a template in single-email mode - if template_id: - update_values = composer._onchange_template_id( - template_id, - kwargs['composition_mode'], - self._name, - res_id - )['value'] - composer.write(update_values) - return composer._action_send_mail(auto_commit=auto_commit) + mails_su = self.env['mail.mail'].sudo() + for subset in [self] if template else self: + composer_ctx['active_ids'] = subset.ids # still required for mass mail mode + if template: + composer_ctx['default_res_id'] = subset.ids[0] if subset.ids else 0 + else: + composer_values['body'] = bodies[subset.id] + composer_ctx['default_res_id'] = subset.id + + composer = self.env['mail.compose.message'].with_context( + **composer_ctx + ).create(composer_values) + # Simulate the onchange (like trigger in form the view) only + # when having a template in single-email mode + if template: + update_values = composer._onchange_template_id( + template.id, + composer_values['composition_mode'], + self._name, + subset.ids[0] if subset.ids else 0 + )['value'] + composer.write(update_values) + + mails_as_sudo, _messages_as_sudo = composer._action_send_mail(auto_commit=auto_commit) + mails_su += mails_as_sudo + return mails_su + + def message_post_with_source(self, source_ref, render_values=None, + message_type='notification', + subtype_xmlid=False, subtype_id=False, + **kwargs): + """ Post a message on each record of self, using a view to render the + body using QWeb. + + Default values + * subtype_id: if not given, fallback on ``note`` to be consistent + with what message_post does; + + :param record/str source_ref: reference to a source for rendering. + It can be one of + * a MailTemplate record. It will be used to render the various + message values (body, subject, recipients, ...). It should behave + like using the mail composer with a template; + * an IrUIView record. It will be used to render the content + (body). Other fields are left to the caller and/or default values + computation; + * an XmlID of a MailTemplate or of an IrUiView: see above + :param dict render_values: additional rendering values for qweb context; + + :param str message_type: one of 'notification' or 'comment'; + :param str subtype_xmlid: optional xml id of a mail.message.subtype to + fetch, will force value of subtype_id; + :param int subtype_id: subtype_id of the message, used mainly for followers + notification mechanism; + :param dict kwargs: additional values given to the 'mail.compose.message' + creation; + + :return: posted mail.message records + """ + template, view = self._get_source_from_ref(source_ref) + + # preliminary value safety check + self._raise_for_invalid_parameters( + set(kwargs.keys()), + forbidden_names={'body', 'composition_mode', 'model', 'res_id', 'values'} + ) + + # with a view, render bodies in batch (template is managed by composer) + bodies = self.env['mail.render.mixin']._render_template_qweb_view( + view, + self._name, + self.ids, + add_context=render_values, + ) if view else {} + + # Prepare composer values for creation + if subtype_xmlid: + subtype_id = self.env['ir.model.data']._xmlid_to_res_id(subtype_xmlid) + if not subtype_id: + subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note') + + messages = self.env['mail.message'] + for record in self: + if template: + composer = self.env['mail.compose.message'].with_context( + active_ids=record.ids, # still required for mass mail mode + default_composition_mode='comment', + default_model=self._name, + default_res_id=record.id, + default_template_id=template.id, + ).create({ + 'message_type': message_type, + 'subtype_id': subtype_id, + **kwargs, + }) + + # Simulate the onchange (like trigger in form the view) to do the + # rendering in mono-record mode + update_values = composer._onchange_template_id( + template.id, + 'comment', + self._name, + record.id, + )['value'] + composer.write(update_values) + _mails_as_sudo, messages_as_sudo = composer._action_send_mail() + messages += messages_as_sudo + else: + messages += record.message_post( + body=bodies[record.id], + message_type=message_type, + subtype_id=subtype_id, + **kwargs + ) + return messages def message_notify(self, *, partner_ids=False, parent_id=False, model=False, res_id=False, author_id=None, email_from=None, body='', subject=False, subtype_xmlid=None, **kwargs): - """ Shortcut allowing to notify partners of messages that shouldn't be - displayed on a document. It pushes notifications on inbox or by email depending - on the user configuration, like other notifications. """ + """ Shortcut allowing to notify partners of messages that should not be + displayed on a document. It pushes notifications on inbox or by email + depending on the user configuration, like other notifications. + + Default values + * subtype_id: if not given, fallback on ``note`` to be consistent + with what message_post does; + + :return: posted mail.message records + """ if self: self.ensure_one() if not partner_ids: @@ -2245,47 +2358,51 @@ class MailThread(models.AbstractModel): self._notify_thread(new_message, msg_values, **notif_kwargs) return new_message - def _message_log_with_view(self, views_or_xmlid, **kwargs): - """ Helper method to log a note using a view_id without notifying followers. """ - return self._message_compose_with_view(views_or_xmlid, message_log=True, **kwargs) + def _message_log_with_view(self, view_ref, render_values=None, + message_type='notification', **kwargs): + """ Log a message on each record of self, using a view to render the + body using QWeb. + + :param str/int/record view_ref: source QWeb template. It should be an + XmlID allowing to fetch an ``ir.ui.view``, or an ID of a view or + an ``ir.ui.view`` record; + :param dict render_values: additional rendering values for qweb context; + :param str message_type: one of 'notification' or 'comment'; + :param kwargs: additional values propagated to ``_message_log``; + + :return: posted mail.message records (as sudo) + """ + self._raise_for_invalid_parameters( + set(kwargs.keys()), + forbidden_names={'body', 'bodies'} + ) + + # with a view, render bodies in batch (template is managed by composer) + bodies = self.env['mail.render.mixin']._render_template_qweb_view( + view_ref, + self._name, + self.ids, + add_context=render_values, + ) + + return self._message_log_batch( + bodies=bodies, + message_type=message_type, + **kwargs + ) def _message_log(self, *, body='', author_id=None, email_from=None, subject=False, message_type='notification', attachment_ids=False, tracking_value_ids=False): - """ Shortcut allowing to post note on a document. It does not perform - any notification and pre-computes some values to have a short code - as optimized as possible. This method is private as it does not check - access rights and perform the message creation as sudo to speedup - the log process. This method should be called within methods where - access rights are already granted to avoid privilege escalation. - - :return: created message (as sudo)""" + """ Shortcut allowing to post note on a document. See ``_message_log_batch`` + for more details. """ self.ensure_one() - author_id, email_from = self._message_compute_author(author_id, email_from, raise_on_email=False) - msg_values = { - # author - 'author_id': author_id, - 'email_from': email_from, - # document - 'model': self._name, - 'record_name': False, - 'res_id': self.id, - # content - 'attachment_ids': attachment_ids, - 'body': body, - 'is_internal': True, - 'message_type': message_type, - 'subject': subject, - 'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'), - 'tracking_value_ids': tracking_value_ids, - # recipients - 'email_add_signature': False, # False as no notification -> no need to compute signature - 'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify - 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False], - } - - return self.sudo()._message_create([msg_values]) + return self._message_log_batch( + {self.id: body}, author_id=author_id, email_from=email_from, + subject=subject, message_type=message_type, + attachment_ids=attachment_ids, tracking_value_ids=tracking_value_ids + ) def _message_log_batch(self, bodies, author_id=None, email_from=None, subject=False, message_type='notification', @@ -2326,6 +2443,7 @@ class MailThread(models.AbstractModel): 'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False], } + values_list = [dict(base_message_values, res_id=record.id, body=bodies.get(record.id, '')) @@ -2401,6 +2519,8 @@ class MailThread(models.AbstractModel): return self.name_get()[0][1] def _message_create(self, values_list): + """ Low-level helper to create mail.message records. It is mainly used + to hide the cleanup of given values, for mail gateway or helpers.""" create_values_list = [] # preliminary value safety check @@ -2455,6 +2575,61 @@ class MailThread(models.AbstractModel): 'tracking_value_ids', } + def _get_source_from_ref(self, source_ref): + """ From a source_reference, return either a mail template, either + an ir ui view. + + :return tuple(template, view): one is a recordset (may be void if + source_ref is a void recordset, or a singleton), the other one is + False. Always only one is set, as source is either a template, + either a view. + """ + template, view = False, False + if isinstance(source_ref, models.BaseModel): + if source_ref._name == 'mail.template': + template = source_ref + elif source_ref._name == 'ir.ui.view': + view = source_ref + else: + raise ValueError( + _('Invalid template or view source record %(svalue)s, is %(model)s instead', + svalue=source_ref, + model=source_ref._name, + )) + if not template and not view: + raise ValueError( + _('Mailing or posting with a source should not be called with an empty %(source_type)s', + source_type=_('template') if template is not False else _('view')) + ) + elif isinstance(source_ref, str): + try: + res_model, res_id = self.env['ir.model.data']._xmlid_to_res_model_res_id( + source_ref, + raise_if_not_found=True + ) + except ValueError as e: + raise ValueError( + _('Invalid template or view source Xml ID %(source_ref)s does not exist anymore', + source_ref=source_ref) + ) from e + if res_model == 'mail.template': + template = self.env['mail.template'].browse(res_id) + elif res_model == 'ir.ui.view': + view = self.env['ir.ui.view'].browse(res_id) + else: + raise ValueError( + _('Invalid template or view source reference %(svalue)s, is %(model)s instead', + svalue=source_ref, + model=res_model, + )) + else: + raise ValueError( + _('Invalid template or view source %(svalue)s (type %(stype)s), should be a record or an XMLID', + svalue=source_ref, + stype=type(source_ref), + )) + return template, view + def _get_notify_valid_parameters(self): """ Several parameters exist for notification methods as business flows often want to customize the standard notification experience. diff --git a/addons/mail_group/models/mail_group.py b/addons/mail_group/models/mail_group.py index 42aba8fb070..5f3bbde3901 100644 --- a/addons/mail_group/models/mail_group.py +++ b/addons/mail_group/models/mail_group.py @@ -314,7 +314,7 @@ class MailGroup(models.Model): values )) - mail_message = Mailthread._message_create(values) + mail_message = Mailthread._message_create([values]) # Find the parent group_message_parent_id = False diff --git a/addons/mail_plugin/controllers/mail_plugin.py b/addons/mail_plugin/controllers/mail_plugin.py index 22a5506302a..4ca6c7e426f 100644 --- a/addons/mail_plugin/controllers/mail_plugin.py +++ b/addons/mail_plugin/controllers/mail_plugin.py @@ -117,9 +117,9 @@ class MailPluginController(http.Controller): partner.write(partner_values) - partner.message_post_with_view( + partner.message_post_with_source( 'iap_mail.enrich_company', - values=iap_data, + render_values=iap_data, subtype_xmlid='mail.mt_note', ) @@ -357,9 +357,9 @@ class MailPluginController(http.Controller): new_company = request.env['res.partner'].create(new_company_info) - new_company.message_post_with_view( + new_company.message_post_with_source( 'iap_mail.enrich_company', - values=iap_data, + render_values=iap_data, subtype_xmlid='mail.mt_note', ) diff --git a/addons/mass_mailing/models/mail_thread.py b/addons/mass_mailing/models/mail_thread.py index 7c98349dab6..78fe220f218 100644 --- a/addons/mass_mailing/models/mail_thread.py +++ b/addons/mass_mailing/models/mail_thread.py @@ -28,13 +28,21 @@ class MailThread(models.AbstractModel): self.env['mailing.trace'].set_replied(domain=[('message_id', 'in', msg_references)]) return super(MailThread, self)._message_route_process(message, message_dict, routes) - def message_post_with_template(self, template_id, **kwargs): + def message_mail_with_source(self, source_ref, **kwargs): # avoid having message send through `message_post*` methods being implicitly considered as # mass-mailing return super(MailThread, self.with_context( default_mass_mailing_name=False, default_mass_mailing_id=False, - )).message_post_with_template(template_id, **kwargs) + )).message_mail_with_source(source_ref, **kwargs) + + def message_post_with_source(self, source_ref, **kwargs): + # avoid having message send through `message_post*` methods being implicitly considered as + # mass-mailing + return super(MailThread, self.with_context( + default_mass_mailing_name=False, + default_mass_mailing_id=False, + )).message_post_with_source(source_ref, **kwargs) @api.model def _routing_handle_bounce(self, email_message, message_dict): diff --git a/addons/mrp/models/stock_rule.py b/addons/mrp/models/stock_rule.py index 58e5c7d0a56..44403e8086a 100644 --- a/addons/mrp/models/stock_rule.py +++ b/addons/mrp/models/stock_rule.py @@ -65,15 +65,15 @@ class StockRule(models.Model): subtype_id=note_subtype_id ) elif orderpoint: - production.message_post_with_view( + production.message_post_with_source( 'mail.message_origin_link', - values={'self': production, 'origin': orderpoint}, + render_values={'self': production, 'origin': orderpoint}, subtype_id=note_subtype_id, ) elif origin_production: - production.message_post_with_view( + production.message_post_with_source( 'mail.message_origin_link', - values={'self': production, 'origin': origin_production}, + render_values={'self': production, 'origin': origin_production}, subtype_id=note_subtype_id, ) return True diff --git a/addons/partner_autocomplete/models/res_company.py b/addons/partner_autocomplete/models/res_company.py index b033898e3a7..b2c52162548 100644 --- a/addons/partner_autocomplete/models/res_company.py +++ b/addons/partner_autocomplete/models/res_company.py @@ -81,9 +81,9 @@ class ResCompany(models.Model): if additional_data: template_values = json.loads(additional_data) template_values['flavor_text'] = _("Company auto-completed by Odoo Partner Autocomplete Service") - self.partner_id.message_post_with_view( + self.partner_id.message_post_with_source( 'iap_mail.enrich_company', - values=template_values, + render_values=template_values, subtype_xmlid='mail.mt_note', ) return True diff --git a/addons/partner_autocomplete/models/res_partner.py b/addons/partner_autocomplete/models/res_partner.py index 8de11eeaa09..6bef55c8787 100644 --- a/addons/partner_autocomplete/models/res_partner.py +++ b/addons/partner_autocomplete/models/res_partner.py @@ -166,9 +166,9 @@ class ResPartner(models.Model): if partners.additional_info: template_values = json.loads(partners.additional_info) template_values['flavor_text'] = _("Partner created by Odoo Partner Autocomplete Service") - partners.message_post_with_view( + partners.message_post_with_source( 'iap_mail.enrich_company', - values=template_values, + render_values=template_values, subtype_xmlid='mail.mt_note', ) partners.write({'additional_info': False}) diff --git a/addons/portal/wizard/portal_share.py b/addons/portal/wizard/portal_share.py index 1bc76e32418..2d942afd964 100644 --- a/addons/portal/wizard/portal_share.py +++ b/addons/portal/wizard/portal_share.py @@ -64,14 +64,14 @@ class PortalShare(models.TransientModel): share_link = self.resource_ref.get_base_url() + self.resource_ref._get_share_url(redirect=True, pid=partner.id) saved_lang = self.env.lang self = self.with_context(lang=partner.lang) - template = self.env.ref('portal.portal_share_template', False) - self.resource_ref.message_post_with_view(template, - values={'partner': partner, 'note': self.note, 'record': self.resource_ref, + self.resource_ref.message_post_with_source( + 'portal.portal_share_template', + render_values={'partner': partner, 'note': self.note, 'record': self.resource_ref, 'share_link': share_link}, subject=_("You are invited to access %s", self.resource_ref.display_name), subtype_xmlid='mail.mt_note', email_layout_xmlid='mail.mail_notification_light', - partner_ids=[(6, 0, partner.ids)]) + partner_ids=partner.ids) self = self.with_context(lang=saved_lang) def _send_signup_link(self, partners=None): @@ -83,14 +83,14 @@ class PortalShare(models.TransientModel): share_link = partner._get_signup_url_for_action(action='/mail/view', res_id=self.res_id, model=self.res_model)[partner.id] saved_lang = self.env.lang self = self.with_context(lang=partner.lang) - template = self.env.ref('portal.portal_share_template', False) - self.resource_ref.message_post_with_view(template, - values={'partner': partner, 'note': self.note, 'record': self.resource_ref, + self.resource_ref.message_post_with_source( + 'portal.portal_share_template', + render_values={'partner': partner, 'note': self.note, 'record': self.resource_ref, 'share_link': share_link}, subject=_("You are invited to access %s", self.resource_ref.display_name), subtype_xmlid='mail.mt_note', email_layout_xmlid='mail.mail_notification_light', - partner_ids=[(6, 0, partner.ids)]) + partner_ids=partner.ids) self = self.with_context(lang=saved_lang) def action_send_mail(self): diff --git a/addons/product_email_template/models/account_move.py b/addons/product_email_template/models/account_move.py index 0c240542111..798b650f6dc 100644 --- a/addons/product_email_template/models/account_move.py +++ b/addons/product_email_template/models/account_move.py @@ -18,9 +18,8 @@ class AccountMove(models.Model): comment_subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment') for line in invoice.invoice_line_ids: if line.product_id.email_template_id: - invoice.message_post_with_template( - line.product_id.email_template_id.id, - composition_mode="comment", + invoice.message_post_with_source( + line.product_id.email_template_id, email_layout_xmlid="mail.mail_notification_light", subtype_id=comment_subtype_id, ) diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index d8ff5c665ef..108e28d89f1 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -750,8 +750,8 @@ class PurchaseOrder(models.Model): if send_single: return order._send_reminder_open_composer(template.id) else: - order.with_context(is_reminder=True).message_post_with_template( - template.id, + order.with_context(is_reminder=True).message_post_with_source( + template, email_layout_xmlid="mail.mail_notification_layout_with_responsible_signature", subtype_xmlid='mail.mt_comment', ) @@ -1104,9 +1104,9 @@ class PurchaseOrderLine(models.Model): if 'product_qty' in values: for line in self: if line.order_id.state == 'purchase': - line.order_id.message_post_with_view( + line.order_id.message_post_with_source( 'purchase.track_po_line_template', - values={'line': line, 'product_qty': values['product_qty']}, + render_values={'line': line, 'product_qty': values['product_qty']}, subtype_xmlid='mail.mt_note', ) if 'qty_received' in values: @@ -1425,9 +1425,9 @@ class PurchaseOrderLine(models.Model): def _track_qty_received(self, new_qty): self.ensure_one() if new_qty != self.qty_received and self.order_id.state == 'purchase': - self.order_id.message_post_with_view( + self.order_id.message_post_with_source( 'purchase.track_po_line_qty_received_template', - values={'line': self, 'qty_received': new_qty}, + render_values={'line': self, 'qty_received': new_qty}, subtype_xmlid='mail.mt_note', ) diff --git a/addons/purchase_requisition/models/purchase.py b/addons/purchase_requisition/models/purchase.py index 9f36e77e47b..f771d99c8d1 100644 --- a/addons/purchase_requisition/models/purchase.py +++ b/addons/purchase_requisition/models/purchase.py @@ -144,9 +144,9 @@ class PurchaseOrder(models.Model): self.env['purchase.order.group'].create({'order_ids': [Command.set(origin_po_id.ids + orders.ids)]}) for order in orders: if order.requisition_id: - order.message_post_with_view( + order.message_post_with_source( 'mail.message_origin_link', - values={'self': order, 'origin': order.requisition_id}, + render_values={'self': order, 'origin': order.requisition_id}, subtype_xmlid='mail.mt_note', ) return orders @@ -158,9 +158,9 @@ class PurchaseOrder(models.Model): result = super(PurchaseOrder, self).write(vals) if vals.get('requisition_id'): for order in self: - order.message_post_with_view( + order.message_post_with_source( 'mail.message_origin_link', - values={'self': order, 'origin': order.requisition_id, 'edit': True}, + render_values={'self': order, 'origin': order.requisition_id, 'edit': True}, subtype_xmlid='mail.mt_note', ) if vals.get('alternative_po_ids', False): diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py index 0ce54cb39ab..642dc4212dc 100644 --- a/addons/purchase_stock/models/purchase.py +++ b/addons/purchase_stock/models/purchase.py @@ -251,8 +251,9 @@ class PurchaseOrder(models.Model): # Get following pickings (created by push rules) to confirm them as well. forward_pickings = self.env['stock.picking']._get_impacted_pickings(moves) (pickings | forward_pickings).action_confirm() - picking.message_post_with_view('mail.message_origin_link', - values={'self': picking, 'origin': order}, + picking.message_post_with_source( + 'mail.message_origin_link', + render_values={'self': picking, 'origin': order}, subtype_xmlid='mail.mt_note', ) return True diff --git a/addons/rating/models/rating_mixin.py b/addons/rating/models/rating_mixin.py index 6c60ecda73a..599fe908804 100644 --- a/addons/rating/models/rating_mixin.py +++ b/addons/rating/models/rating_mixin.py @@ -181,12 +181,11 @@ class RatingMixin(models.AbstractModel): """ if lang: template = template.with_context(lang=lang) - for record in self.with_context(mail_notify_force_send=force_send): - record.message_post_with_template( - template.id, - email_layout_xmlid='mail.mail_notification_light', - subtype_xmlid='mail.mt_note', - ) + self.with_context(mail_notify_force_send=force_send).message_post_with_source( + template, + email_layout_xmlid='mail.mail_notification_light', + subtype_xmlid='mail.mt_note', + ) def rating_apply(self, rate, token=None, rating=None, feedback=None, subtype_xmlid=None, notify_delay_send=False): diff --git a/addons/sale/models/payment_transaction.py b/addons/sale/models/payment_transaction.py index a5364d45a2e..4ad6a75cb54 100644 --- a/addons/sale/models/payment_transaction.py +++ b/addons/sale/models/payment_transaction.py @@ -131,6 +131,9 @@ class PaymentTransaction(models.Model): )) if not template_id: return + template = self.env['mail.template'].browse(template_id).exists() + if not template: + return for tx in self: tx = tx.with_company(tx.company_id).with_context( @@ -140,12 +143,11 @@ class PaymentTransaction(models.Model): lambda i: not i.is_move_sent and i.state == 'posted' and i._is_ready_to_be_sent() ) invoice_to_send.is_move_sent = True # Mark invoice as sent - for invoice in invoice_to_send.with_user(SUPERUSER_ID): - invoice.message_post_with_template( - template_id, - email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature', - subtype_xmlid='mail.mt_comment', - ) + invoice_to_send.with_user(SUPERUSER_ID).message_post_with_source( + template, + email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature', + subtype_xmlid='mail.mt_comment', + ) def _cron_send_invoice(self): """ diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index caa59ffa6b4..cc506c77b41 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -842,8 +842,8 @@ class SaleOrder(models.Model): mail_template = sale_order._get_confirmation_template() if not mail_template: continue - sale_order.with_context(force_send=True).message_post_with_template( - mail_template.id, + sale_order.with_context(force_send=True).message_post_with_source( + mail_template, email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature', subtype_xmlid='mail.mt_comment', ) @@ -1181,9 +1181,9 @@ class SaleOrder(models.Model): if final: moves.sudo().filtered(lambda m: m.amount_total < 0).action_switch_move_type() for move in moves: - move.message_post_with_view( + move.message_post_with_source( 'mail.message_origin_link', - values={'self': move, 'origin': move.line_ids.sale_line_ids.order_id}, + render_values={'self': move, 'origin': move.line_ids.sale_line_ids.order_id}, subtype_xmlid='mail.mt_note', ) return moves diff --git a/addons/sale/tests/test_controllers.py b/addons/sale/tests/test_controllers.py index 5c828e479bf..3dba4c73252 100644 --- a/addons/sale/tests/test_controllers.py +++ b/addons/sale/tests/test_controllers.py @@ -87,8 +87,8 @@ class TestSaleSignature(HttpCaseWithUserPortal): # must be sent to the user so he can see it email_act = sales_order.action_quotation_send() email_ctx = email_act.get('context', {}) - sales_order.with_context(**email_ctx).message_post_with_template( - email_ctx.get('default_template_id'), + sales_order.with_context(**email_ctx).message_post_with_source( + self.env['mail.template'].browse(email_ctx.get('default_template_id')), subtype_xmlid='mail.mt_comment', ) diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index aa38876de75..22a8b31c318 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -79,8 +79,8 @@ class TestSaleOrder(SaleCommon): # send quotation email_act = self.sale_order.action_quotation_send() email_ctx = email_act.get('context', {}) - self.sale_order.with_context(**email_ctx).message_post_with_template( - email_ctx.get('default_template_id'), + self.sale_order.with_context(**email_ctx).message_post_with_source( + self.env['mail.template'].browse(email_ctx.get('default_template_id')), subtype_xmlid='mail.mt_comment', ) self.assertTrue(self.sale_order.state == 'sent', 'Sale: state after sending is wrong') @@ -104,8 +104,8 @@ class TestSaleOrder(SaleCommon): # sent to to author or not (in case author is present in 'Recipients' of composer). mail_template = self.env['mail.template'].browse(email_ctx.get('default_template_id')).copy({'auto_delete': False}) # send the mail with same user as customer - sale_order.with_context(**email_ctx).with_user(self.sale_user).message_post_with_template( - mail_template.id, + sale_order.with_context(**email_ctx).with_user(self.sale_user).message_post_with_source( + mail_template, subtype_xmlid='mail.mt_comment', ) self.assertTrue(sale_order.state == 'sent', 'Sale : state should be changed to sent') diff --git a/addons/sale/tests/test_sale_to_invoice.py b/addons/sale/tests/test_sale_to_invoice.py index c205ae94fc6..a7851b69451 100644 --- a/addons/sale/tests/test_sale_to_invoice.py +++ b/addons/sale/tests/test_sale_to_invoice.py @@ -632,8 +632,8 @@ class TestSaleToInvoice(TestSaleCommon): # send quotation email_act = self.sale_order.action_quotation_send() email_ctx = email_act.get('context', {}) - self.sale_order.with_context(**email_ctx).message_post_with_template( - email_ctx.get('default_template_id'), + self.sale_order.with_context(**email_ctx).message_post_with_source( + self.env['mail.template'].browse(email_ctx.get('default_template_id')), subtype_xmlid='mail.mt_comment', ) self.assertTrue(self.sale_order.state == 'sent', 'Sale: state after sending is wrong') diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index 7382c645c14..501ab2e3d00 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -186,9 +186,9 @@ class SaleAdvancePaymentInv(models.TransientModel): self._prepare_invoice_values(order, down_payment_so_line) ).with_user(self.env.uid) # Unsudo the invoice after creation - invoice.message_post_with_view( + invoice.message_post_with_source( 'mail.message_origin_link', - values={'self': invoice, 'origin': order}, + render_values={'self': invoice, 'origin': order}, subtype_xmlid='mail.mt_note', ) diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index 6fb7d1db1dc..50ad69b5c21 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -42,9 +42,9 @@ class StockMove(models.Model): picking_id = self.mapped('picking_id') sale_order_ids = self.mapped('sale_line_id.order_id') for sale_order_id in sale_order_ids: - picking_id.message_post_with_view( + picking_id.message_post_with_source( 'mail.message_origin_link', - values={'self': picking_id, 'origin': sale_order_id}, + render_values={'self': picking_id, 'origin': sale_order_id}, subtype_xmlid='mail.mt_note', ) diff --git a/addons/sale_stock_margin/tests/test_sale_stock_margin.py b/addons/sale_stock_margin/tests/test_sale_stock_margin.py index 77a6ca3a20e..75228136e3b 100644 --- a/addons/sale_stock_margin/tests/test_sale_stock_margin.py +++ b/addons/sale_stock_margin/tests/test_sale_stock_margin.py @@ -310,7 +310,10 @@ class TestSaleStockMargin(TestStockValuationCommon): so = so_form.save() email_act = so.action_quotation_send() email_ctx = email_act.get('context', {}) - so.with_context(**email_ctx).message_post_with_template(email_ctx.get('default_template_id')) + so.with_context(**email_ctx).message_post_with_source( + self.env['mail.template'].browse(email_ctx.get('default_template_id')), + subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'), + ) self.assertEqual(so.state, 'sent') self.assertEqual(so.order_line[0].purchase_price, 15) diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 1e57a205494..63da5af9af0 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -667,9 +667,9 @@ class StockMoveLine(models.Model): data['result_package_name'] = self.env['stock.quant.package'].browse(vals.get('result_package_id')).name if 'owner_id' in vals and vals['owner_id'] != move.owner_id.id: data['owner_name'] = self.env['res.partner'].browse(vals.get('owner_id')).name - record.message_post_with_view( + record.message_post_with_source( template, - values={'move': move, 'vals': dict(vals, **data)}, + render_values={'move': move, 'vals': dict(vals, **data)}, subtype_xmlid='mail.mt_note', ) diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index bf3668979f7..1ca08ec2d59 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -886,9 +886,9 @@ class Picking(models.Model): def _send_confirmation_email(self): subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment') for stock_pick in self.filtered(lambda p: p.company_id.stock_move_email_validation and p.picking_type_id.code == 'outgoing'): - delivery_template_id = stock_pick.company_id.stock_mail_confirmation_template_id.id - stock_pick.with_context(force_send=True).message_post_with_template( - delivery_template_id, + delivery_template = stock_pick.company_id.stock_mail_confirmation_template_id + stock_pick.with_context(force_send=True).message_post_with_source( + delivery_template, email_layout_xmlid='mail.mail_notification_light', subtype_id=subtype_id, ) diff --git a/addons/stock/wizard/stock_picking_return.py b/addons/stock/wizard/stock_picking_return.py index 1b78158de32..80db63fcc54 100644 --- a/addons/stock/wizard/stock_picking_return.py +++ b/addons/stock/wizard/stock_picking_return.py @@ -133,9 +133,9 @@ class ReturnPicking(models.TransientModel): # create new picking for returned products new_picking = self.picking_id.copy(self._prepare_picking_default_values()) picking_type_id = new_picking.picking_type_id.id - new_picking.message_post_with_view( + new_picking.message_post_with_source( 'mail.message_origin_link', - values={'self': new_picking, 'origin': self.picking_id}, + render_values={'self': new_picking, 'origin': self.picking_id}, subtype_xmlid='mail.mt_note', ) returned_lines = 0 diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py index 389e92a54a2..2e1f05ccf0b 100644 --- a/addons/test_mail/tests/test_message_post.py +++ b/addons/test_mail/tests/test_message_post.py @@ -404,6 +404,7 @@ class TestMessageNotify(TestMessagePostCommon): ) @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests') def test_notify_parameters(self): """ Test usage of parameters in notify, both for unwanted side effects and magic parameters. """ @@ -536,7 +537,7 @@ class TestMessageLog(TestMessagePostCommon): new_notes = test_records._message_log_with_view( 'test_mail.mail_template_simple_test', - values={'partner': self.user_employee.partner_id} + render_values={'partner': self.user_employee.partner_id} ) for test_record, new_note in zip(test_records, new_notes): self.assertMessageFields( @@ -1031,6 +1032,30 @@ class TestMessagePostHelpers(TestMessagePostCommon): # Force the attachments of the template to be in the natural order. cls.test_template.invalidate_recordset(['attachment_ids']) + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_message_helpers_source_ref(self): + """ Test various sources (record or xml id) to ensure source_ref right + computation. """ + test_records = self.test_records.with_env(self.env) + template = self.test_template.with_env(self.env) + view = self.env.ref('test_mail.mail_template_simple_test') + + for source_ref in ('test_mail.mail_test_ticket_tracking_tpl', template, + 'test_mail.mail_template_simple_test', view): + with self.subTest(source_ref=source_ref), self.mock_mail_gateway(): + _new_mails = test_records.with_user(self.user_employee).message_mail_with_source( + source_ref, + render_values={'partner': self.user_employee.partner_id}, + subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'), + ) + + _new_messages = test_records.with_user(self.user_employee).message_post_with_source( + source_ref, + render_values={'partner': self.user_employee.partner_id}, + subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'), + ) + @users('employee') @mute_logger('odoo.addons.mail.models.mail_mail') def test_message_mail_with_template(self): @@ -1038,9 +1063,8 @@ class TestMessagePostHelpers(TestMessagePostCommon): test_records = self.test_records.with_env(self.env) template = self.test_template.with_env(self.env) with self.mock_mail_gateway(): - _new_mails, _new_messages = test_records.with_user(self.user_employee).message_post_with_template( - template.id, - composition_mode='mass_mail', + _new_mails = test_records.with_user(self.user_employee).message_mail_with_source( + template, subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'), ) @@ -1086,14 +1110,13 @@ class TestMessagePostHelpers(TestMessagePostCommon): test_record.message_subscribe(test_record.customer_id.ids) with self.mock_mail_gateway(): - new_messages = test_records.message_post_with_view( + new_mails = test_records.message_mail_with_source( 'test_mail.mail_template_simple_test', - values={'partner': self.user_employee.partner_id}, - composition_mode='mass_mail', + render_values={'partner': self.user_employee.partner_id}, subject='About mass mailing', subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'), ) - self.assertEqual(len(new_messages), 0) + self.assertEqual(len(new_mails), 10) self.assertEqual(len(self._new_mails), 10) # sent emails (mass mail mode) @@ -1120,6 +1143,19 @@ class TestMessagePostHelpers(TestMessagePostCommon): } ) + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_message_post_with_source_subtype(self): + """ Test subtype tweaks when posting with a source """ + test_record = self.test_records.with_env(self.env)[0] + test_template = self.test_template.with_env(self.env) + with self.mock_mail_gateway(): + new_message = test_record.with_user(self.user_employee).message_post_with_source( + test_template, + subtype_xmlid='mail.mt_activities', + ) + self.assertEqual(new_message.subtype_id, self.env.ref("mail.mt_activities")) + @users('employee') @mute_logger('odoo.addons.mail.models.mail_mail') def test_message_post_with_template(self): @@ -1128,9 +1164,8 @@ class TestMessagePostHelpers(TestMessagePostCommon): test_record.message_subscribe(test_record.customer_id.ids) test_template = self.test_template.with_env(self.env) with self.mock_mail_gateway(): - _new_mail, new_message = test_record.with_user(self.user_employee).message_post_with_template( - test_template.id, - composition_mode='comment', + new_message = test_record.with_user(self.user_employee).message_post_with_source( + test_template, message_type='comment', subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'), ) @@ -1165,15 +1200,15 @@ class TestMessagePostHelpers(TestMessagePostCommon): ) @users('employee') - @mute_logger('odoo.addons.mail.models.mail') + @mute_logger('odoo.addons.mail.models.mail_mail') def test_message_post_with_template_defaults(self): """ Test default values, notably subtype being a comment """ test_record = self.test_records.with_env(self.env)[0] test_record.message_subscribe(test_record.customer_id.ids) test_template = self.test_template.with_env(self.env) with self.mock_mail_gateway(): - _new_mail, new_message = test_record.with_user(self.user_employee).message_post_with_template( - test_template.id, + new_message = test_record.with_user(self.user_employee).message_post_with_source( + test_template, ) # created partners from inline email addresses @@ -1192,7 +1227,7 @@ class TestMessagePostHelpers(TestMessagePostCommon): {'partner': new_partners[1], 'type': 'email'}, {'partner': test_record.customer_id, 'type': 'email'}, ], - 'subtype': 'mail.mt_comment', + 'subtype': 'mail.mt_note', }]) self.assertMessageFields( new_message, @@ -1213,10 +1248,10 @@ class TestMessagePostHelpers(TestMessagePostCommon): test_record.message_subscribe(test_record.customer_id.ids) with self.mock_mail_gateway(): - new_message = test_record.message_post_with_view( + new_message = test_record.message_post_with_source( 'test_mail.mail_template_simple_test', message_type='comment', - values={'partner': self.user_employee.partner_id}, + render_values={'partner': self.user_employee.partner_id}, subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'), ) @@ -1250,9 +1285,9 @@ class TestMessagePostHelpers(TestMessagePostCommon): # defaults is a note, take into account specified recipients with self.mock_mail_gateway(): - new_message = test_record.message_post_with_view( + new_message = test_record.message_post_with_source( 'test_mail.mail_template_simple_test', - values={'partner': self.user_employee.partner_id}, + render_values={'partner': self.user_employee.partner_id}, partner_ids=test_record.customer_id.ids, ) @@ -1263,7 +1298,7 @@ class TestMessagePostHelpers(TestMessagePostCommon): 'notif': [ {'partner': test_record.customer_id, 'type': 'email'}, ], - 'subtype': 'mail.mt_comment', + 'subtype': 'mail.mt_note', }]) self.assertMessageFields( new_message, @@ -1340,9 +1375,8 @@ class TestMessagePostLang(TestMailCommon, TestRecipients): test_template = self.test_template.with_user(self.env.user) with self.mock_mail_gateway(): - test_record.message_post_with_template( - test_template.id, - composition_mode='comment', + test_record.message_post_with_source( + test_template, email_layout_xmlid='mail.test_layout', message_type='comment', subtype_id=self.env.ref('mail.mt_comment').id, @@ -1382,9 +1416,8 @@ class TestMessagePostLang(TestMailCommon, TestRecipients): test_template = self.test_template.with_user(self.env.user) with self.mock_mail_gateway(): - test_records.message_post_with_template( - test_template.id, - composition_mode='mass_mail', + test_records.message_mail_with_source( + test_template, email_layout_xmlid='mail.test_layout', message_type='comment', subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'), @@ -1448,13 +1481,12 @@ class TestMessagePostLang(TestMailCommon, TestRecipients): test_template = self.test_template.with_user(self.env.user) with self.mock_mail_gateway(): - for test_record in test_records: - test_record.message_post_with_template( - test_template.id, - email_layout_xmlid='mail.test_layout', - message_type='comment', - subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'), - ) + test_records.message_post_with_source( + test_template, + email_layout_xmlid='mail.test_layout', + message_type='comment', + subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'), + ) record0_customer = self.env['res.partner'].search([('email_normalized', '=', 'test.record.1@test.customer.com')], limit=1) self.assertTrue(record0_customer, 'Template usage should have created a contact based on record email') diff --git a/addons/test_mail/tests/test_message_track.py b/addons/test_mail/tests/test_message_track.py index b593381af4d..4aea07103f6 100644 --- a/addons/test_mail/tests/test_message_track.py +++ b/addons/test_mail/tests/test_message_track.py @@ -150,7 +150,7 @@ class TestTracking(TestMailCommon): def patched_message_track_post_template(*args, **kwargs): if args[0]._name == "mail.test.track": - args[0].message_post_with_template(template.id) + args[0].message_post_with_source(template) return True with patch('odoo.addons.mail.models.mail_thread.MailThread._message_track_post_template', patched_message_track_post_template): diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index 956661d8007..c3c98458cd3 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -621,7 +621,7 @@ class TestMailAPIPerformance(BaseMailPerformance): with self.assertQueryCount(__system__=11, employee=11): records._message_log_with_view( 'test_mail.mail_template_simple_test', - values={'partner': self.customer.with_env(self.env)} + render_values={'partner': self.customer.with_env(self.env)} ) @users('__system__', 'employee') @@ -862,14 +862,13 @@ class TestMailComplexPerformance(BaseMailPerformance): def test_complex_message_post_template(self): self.container.message_subscribe(self.user_portal.partner_id.ids) record = self.container.with_user(self.env.user) - template_id = self.env.ref('test_mail.mail_test_container_tpl').id + template = self.env.ref('test_mail.mail_test_container_tpl') # about 20 (19 ?) queries per additional customer group with self.assertQueryCount(__system__=42, employee=43): - record.message_post_with_template( - template_id, + record.message_post_with_source( + template, message_type='comment', - composition_mode='comment', subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'), ) @@ -892,9 +891,9 @@ class TestMailComplexPerformance(BaseMailPerformance): composer._onchange_template_id_wrapper() with self.assertQueryCount(__system__=121, employee=141): - messages_as_sudo = test_records.message_post_with_view( + messages_as_sudo = test_records.message_post_with_source( 'test_mail.mail_template_simple_test', - values={'partner': self.user_test.partner_id}, + render_values={'partner': self.user_test.partner_id}, subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment') ) diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index d9c58799622..1089daab36e 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -221,10 +221,10 @@ class BlogPost(models.Model): def _check_for_publication(self, vals): if vals.get('is_published'): for post in self.filtered(lambda p: p.active): - post.blog_id.message_post_with_view( + post.blog_id.message_post_with_source( 'website_blog.blog_post_template_new_post', subject=post.name, - values={'post': post}, + render_values={'post': post}, subtype_xmlid='website_blog.mt_blog_blog_published', ) return True diff --git a/addons/website_crm_iap_reveal/models/crm_reveal_rule.py b/addons/website_crm_iap_reveal/models/crm_reveal_rule.py index e4d35fa39b2..441d8922c10 100644 --- a/addons/website_crm_iap_reveal/models/crm_reveal_rule.py +++ b/addons/website_crm_iap_reveal/models/crm_reveal_rule.py @@ -376,9 +376,9 @@ class CRMRevealRule(models.Model): 'flavor_text': _("Opportunity created by Odoo Lead Generation"), 'people_data': result.get('people_data'), }) - lead.message_post_with_view( + lead.message_post_with_source( 'iap_mail.enrich_company', - values=template_values, + render_values=template_values, subtype_xmlid='mail.mt_note', ) diff --git a/addons/website_event_track/models/event_track.py b/addons/website_event_track/models/event_track.py index fe6df7e6681..63297d69317 100644 --- a/addons/website_event_track/models/event_track.py +++ b/addons/website_event_track/models/event_track.py @@ -406,9 +406,9 @@ class Track(models.Model): post_values = {} if self.env.user.email else {'email_from': self.env.company.catchall_formatted} for track in tracks: - track.event_id.message_post_with_view( + track.event_id.message_post_with_source( 'website_event_track.event_track_template_new', - values={ + render_values={ 'track': track, 'is_html_empty': is_html_empty, }, diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 6f49b658f58..83bddbd0f69 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -623,17 +623,17 @@ class Post(models.Model): tag_partners = post.tag_ids.sudo().mapped('message_partner_ids') if post.state == 'active' and post.parent_id: - post.parent_id.message_post_with_view( + post.parent_id.message_post_with_source( 'website_forum.forum_post_template_new_answer', subject=_('Re: %s', post.parent_id.name), - partner_ids=[(4, p.id) for p in tag_partners], + partner_ids=tag_partners.ids, subtype_xmlid='website_forum.mt_answer_new', ) elif post.state == 'active' and not post.parent_id: - post.message_post_with_view( + post.message_post_with_source( 'website_forum.forum_post_template_new_question', subject=post.name, - partner_ids=[(4, p.id) for p in tag_partners], + partner_ids=tag_partners.ids, subtype_xmlid='website_forum.mt_question_new', ) elif post.state == 'pending' and not post.parent_id: @@ -642,7 +642,7 @@ class Post(models.Model): partners = post.sudo().message_partner_ids | tag_partners partners = partners.filtered(lambda partner: partner.user_ids and any(user.karma >= post.forum_id.karma_moderate for user in partner.user_ids)) - post.message_post_with_view( + post.message_post_with_source( 'website_forum.forum_post_template_validation', subject=post.name, partner_ids=partners.ids,