diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 269329d4004..7de4485ba16 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -3509,12 +3509,7 @@ class AccountMove(models.Model): compose_form = self.env.ref('account.account_invoice_send_wizard_form', raise_if_not_found=False) ctx = dict( default_model='account.move', - default_res_id=self.id, - # For the sake of consistency we need a default_res_model if - # default_res_id is set. Not renaming default_model as it can - # create many side-effects. - default_res_model='account.move', - default_use_template=bool(template), + default_res_ids=self.ids, default_template_id=template and template.id or False, default_composition_mode='comment', mark_invoice_as_sent=True, diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml index 7d60a621236..38704aadb77 100644 --- a/addons/account/views/account_payment_view.xml +++ b/addons/account/views/account_payment_view.xml @@ -430,7 +430,6 @@ @@ -447,7 +446,6 @@ diff --git a/addons/calendar/models/calendar_event.py b/addons/calendar/models/calendar_event.py index ca3da46a725..9fd4b27f8fd 100644 --- a/addons/calendar/models/calendar_event.py +++ b/addons/calendar/models/calendar_event.py @@ -755,7 +755,6 @@ class Meeting(models.Model): default_composition_mode=default_composition_mode, default_model='calendar.event', default_res_ids=self.ids, - default_use_template=bool(template_id), default_template_id=template_id, default_partner_ids=self.partner_ids.ids, mail_tz=self.env.user.tz, diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 289e4886818..8b8c5730bab 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -735,7 +735,6 @@ new form @@ -748,7 +747,6 @@ new list diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index d1ee3e7a368..22043ef9c5e 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -195,9 +195,8 @@ class EventRegistration(models.Model): compose_form = self.env.ref('mail.email_compose_message_wizard_form') ctx = dict( default_model='event.registration', - default_res_id=self.id, - default_use_template=bool(template), - default_template_id=template and template.id, + default_res_ids=self.ids, + default_template_id=template.id if template else False, default_composition_mode='comment', default_email_layout_xmlid="mail.mail_notification_light", ) diff --git a/addons/hr_presence/models/hr_employee.py b/addons/hr_presence/models/hr_employee.py index 99882f10ad2..28c616b3ef2 100644 --- a/addons/hr_presence/models/hr_employee.py +++ b/addons/hr_presence/models/hr_employee.py @@ -181,8 +181,7 @@ Do not hesitate to contact your manager or the human resource department.""") compose_form = self.env.ref('mail.email_compose_message_wizard_form', False) ctx = dict( default_model="hr.employee", - default_res_id=self.id, - default_use_template=bool(template), + default_res_ids=self.ids, default_template_id=template.id, default_composition_mode='comment', default_is_log=True, diff --git a/addons/loyalty/models/loyalty_card.py b/addons/loyalty/models/loyalty_card.py index 239165b7d52..893980bfc4e 100644 --- a/addons/loyalty/models/loyalty_card.py +++ b/addons/loyalty/models/loyalty_card.py @@ -81,8 +81,7 @@ class LoyaltyCard(models.Model): compose_form = self.env.ref('mail.email_compose_message_wizard_form', False) ctx = dict( default_model='loyalty.card', - default_res_id=self.id, - default_use_template=bool(default_template), + default_res_ids=self.ids, default_template_id=default_template and default_template.id, default_composition_mode='comment', default_email_layout_xmlid='mail.mail_notification_light', diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index d4f4c999944..25c734bbac0 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -170,14 +170,18 @@ class MailTemplate(models.Model): def create_action(self): ActWindow = self.env['ir.actions.act_window'] view = self.env.ref('mail.email_compose_message_wizard_form') - for template in self: + context = { + 'default_composition_mode': 'mass_mail', + 'default_model': template.model, + 'default_template_id' : template.id, + } button_name = _('Send Mail (%s)', template.name) action = ActWindow.create({ 'name': button_name, 'type': 'ir.actions.act_window', 'res_model': 'mail.compose.message', - 'context': "{'default_composition_mode': 'mass_mail', 'default_template_id' : %d, 'default_use_template': True}" % (template.id), + 'context': repr(context), 'view_mode': 'form,tree', 'view_id': view.id, 'target': 'new', diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 4896930c645..c9ff65b6a8b 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -2170,12 +2170,9 @@ class MailThread(models.AbstractModel): 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_ctx['default_res_ids'] = subset.ids + if not template: composer_values['body'] = bodies[subset.id] - composer_ctx['default_res_id'] = subset.id composer = self.env['mail.compose.message'].with_context( **composer_ctx @@ -2185,9 +2182,9 @@ class MailThread(models.AbstractModel): if template: update_values = composer._onchange_template_id( template.id, - composer_values['composition_mode'], + 'mass_mail', self._name, - subset.ids[0] if subset.ids else 0 + subset.ids, )['value'] composer.write(update_values) @@ -2253,10 +2250,9 @@ class MailThread(models.AbstractModel): 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_res_ids=record.ids, default_template_id=template.id, ).create({ 'message_type': message_type, @@ -2270,7 +2266,7 @@ class MailThread(models.AbstractModel): template.id, 'comment', self._name, - record.id, + record.ids, )['value'] composer.write(update_values) _mails_as_sudo, messages_as_sudo = composer._action_send_mail() @@ -3049,11 +3045,12 @@ class MailThread(models.AbstractModel): lang = False if force_email_lang: lang = force_email_lang - elif {'default_template_id', 'default_model', 'default_res_id'} <= self.env.context.keys(): + elif {'default_template_id', 'default_model', 'default_res_ids'} <= self.env.context.keys(): # TDE FIXME: this whole brol should be cleaned ! + res_ids = self.env['mail.compose.message']._parse_res_ids(self.env.context['default_res_ids']) template = self.env['mail.template'].browse(self.env.context['default_template_id']) - if template and template.lang: - lang = template._render_lang([self.env.context['default_res_id']])[self.env.context['default_res_id']] + if res_ids and template and template.lang: + lang = template._render_lang(res_ids)[res_ids[0]] if not lang: lang = self.env.context.get('lang') diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index 20b2de3c972..2cc5a34b94d 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -341,9 +341,8 @@ const KanbanActivity = AbstractField.extend({ views: [[false, "form"]], target: "new", context: { - default_res_id: this.res_id, + default_res_ids: [this.res_id], default_model: this.model, - default_use_template: true, default_template_id: templateID, force_email: true, }, diff --git a/addons/mail/static/src/models/composer_view.js b/addons/mail/static/src/models/composer_view.js index 6ff29361d84..cecc70bdac3 100644 --- a/addons/mail/static/src/models/composer_view.js +++ b/addons/mail/static/src/models/composer_view.js @@ -550,7 +550,7 @@ Model({ default_is_log: this.composer.isLog, default_model: this.composer.activeThread.model, default_partner_ids: this.composer.recipients.map((partner) => partner.id), - default_res_id: this.composer.activeThread.id, + default_res_ids: [this.composer.activeThread.id], default_subtype_xmlid: 'mail.mt_comment', mail_post_autofollow: this.composer.activeThread.hasWriteAccess, }; diff --git a/addons/mail/static/src/models/mail_template.js b/addons/mail/static/src/models/mail_template.js index 100afeac761..53d21f5f4b8 100644 --- a/addons/mail/static/src/models/mail_template.js +++ b/addons/mail/static/src/models/mail_template.js @@ -16,9 +16,8 @@ Model({ views: [[false, "form"]], target: "new", context: { - default_res_id: activity.thread.id, + default_res_ids: [activity.thread.id], default_model: activity.thread.model, - default_use_template: true, default_subtype_xmlid: 'mail.mt_comment', default_template_id: this.id, force_email: true, diff --git a/addons/mail/static/tests/qunit_suite_tests/components/activity_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/activity_tests.js index 96c9506867f..5960101558b 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/activity_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/activity_tests.js @@ -619,7 +619,7 @@ QUnit.module("mail", {}, function () { }); QUnit.test("activity with mail template: preview mail", async function (assert) { - assert.expect(10); + assert.expect(9); const pyEnv = await startServer(); const resPartnerId1 = pyEnv["res.partner"].create({}); @@ -642,20 +642,16 @@ QUnit.module("mail", {}, function () { patchWithCleanup(env.services.action, { doAction(action) { assert.step("do_action"); - assert.strictEqual( - action.context.default_res_id, - resPartnerId1, - "Action should have the activity res id as default res id in context" + assert.deepEqual( + action.context.default_res_ids, + [resPartnerId1], + "Action should have the activity res id as default res ids in context" ); assert.strictEqual( action.context.default_model, "res.partner", "Action should have the activity res model as default model in context" ); - assert.ok( - action.context.default_use_template, - "Action should have true as default use_template in context" - ); assert.strictEqual( action.context.default_template_id, mailTemplateId1, diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index 853ceb611be..7ab6427c9e4 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -69,10 +69,11 @@ class MockEmail(common.BaseCase, MockSmtplibCase): with self.mock_smtplib_connection(), \ patch.object(IrMailServer, 'build_email', autospec=True, wraps=IrMailServer, side_effect=_ir_mail_server_build_email) as build_email_mocked, \ patch.object(IrMailServer, 'send_email', autospec=True, wraps=IrMailServer, side_effect=send_email_origin) as send_email_mocked, \ - patch.object(MailMail, 'create', autospec=True, wraps=MailMail, side_effect=_mail_mail_create), \ + patch.object(MailMail, 'create', autospec=True, wraps=MailMail, side_effect=_mail_mail_create) as mail_mail_create_mocked, \ patch.object(MailMail, 'unlink', autospec=True, wraps=MailMail, side_effect=_mail_mail_unlink): self.build_email_mocked = build_email_mocked self.send_email_mocked = send_email_mocked + self.mail_mail_create_mocked = mail_mail_create_mocked yield if mail_unlink_sent: diff --git a/addons/mail/tests/test_mail_composer.py b/addons/mail/tests/test_mail_composer.py index 3ad1f8c79b0..472fc24d6c5 100644 --- a/addons/mail/tests/test_mail_composer.py +++ b/addons/mail/tests/test_mail_composer.py @@ -84,7 +84,7 @@ class TestMailComposerForm(TestMailComposer): form = Form(self.env['mail.compose.message'].with_context({ 'default_partner_ids': partner_classic.ids, 'default_model': test_record._name, - 'default_res_id': test_record.id, + 'default_res_ids': test_record.ids, })) form.body = '

Hello

' self.assertEqual( @@ -116,7 +116,7 @@ class TestMailComposerForm(TestMailComposer): form = Form(self.env['mail.compose.message'].with_context({ 'default_partner_ids': (partner_private + partner_classic).ids, 'default_model': test_record._name, - 'default_res_id': test_record.id, + 'default_res_ids': test_record.ids, })) form.body = '

Hello

' self.assertEqual( @@ -156,7 +156,7 @@ class TestMailComposerForm(TestMailComposer): _form = Form(self.env['mail.compose.message'].with_context({ 'default_partner_ids': (self.partner_private + partner_classic).ids, 'default_model': test_record._name, - 'default_res_id': test_record.id, + 'default_res_ids': test_record.ids, })) @mute_logger('odoo.addons.mail.models.mail_mail') @@ -177,7 +177,7 @@ class TestMailComposerForm(TestMailComposer): form = Form(self.env['mail.compose.message'].with_context({ 'default_model': test_record._name, - 'default_res_id': test_record.id, + 'default_res_ids': test_record.ids, 'default_template_id': template.id, })) diff --git a/addons/mail/views/res_partner_views.xml b/addons/mail/views/res_partner_views.xml index a3fae6a1d55..377d3589b84 100644 --- a/addons/mail/views/res_partner_views.xml +++ b/addons/mail/views/res_partner_views.xml @@ -111,7 +111,6 @@ 'default_composition_mode': 'mass_mail', 'default_partner_to': '{{ object.id or \'\' }}', 'default_subtype_xmlid': 'mail.mt_comment', - 'default_use_template': False, 'default_reply_to_force_new': True, }"/> diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index e91f0132728..d9547eb929e 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -3,13 +3,15 @@ import ast import base64 -import re +import logging from odoo import _, api, fields, models, tools, Command -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError from odoo.osv import expression from odoo.tools import email_re +_logger = logging.getLogger(__name__) + def _reopen(self, res_id, model, context=None): # save original model in context, because selecting the list of available @@ -41,14 +43,13 @@ class MailComposer(models.TransientModel): _batch_size = 500 @api.model - def default_get(self, fields): - """ Handle composition mode. Some details about context keys: - - comment: default mode, model and ID of a record the user comments - - default_model or active_model - - default_res_id or active_id - - mass_mail: model and IDs of records the user mass-mails - - active_ids: record IDs - - default_model or active_model + def default_get(self, fields_list): + """ Handle composition mode and contextual computation, until moving + to computed fields. Support active_model / active_id(s) as valid default + values, as this comes from standard web client usage. + + Note that supporting active_ids through composer is still done, as we + may have to give a huge list of IDs that won't fit into res_ids field. """ # support subtype xmlid, like ``message_post``, when easier than using ``ref`` if self.env.context.get('default_subtype_xmlid'): @@ -57,12 +58,15 @@ class MailComposer(models.TransientModel): self.env.context['default_subtype_xmlid'] ) ) + # deprecated record context management + if 'default_res_id' in self.env.context: + raise ValueError(_("Deprecated usage of 'default_res_id', should use 'default_res_ids'.")) - result = super(MailComposer, self).default_get(fields) + result = super().default_get(fields_list) # author - missing_author = 'author_id' in fields and 'author_id' not in result - missing_email_from = 'email_from' in fields and 'email_from' not in result + missing_author = 'author_id' in fields_list and 'author_id' not in result + missing_email_from = 'email_from' in fields_list and 'email_from' not in result if missing_author or missing_email_from: author_id, email_from = self.env['mail.thread']._message_compute_author(result.get('author_id'), result.get('email_from'), raise_on_email=False) if missing_email_from: @@ -70,26 +74,36 @@ class MailComposer(models.TransientModel): if missing_author: result['author_id'] = author_id - if 'model' in fields and 'model' not in result: + # record context management + if 'model' in fields_list and 'model' not in result: result['model'] = self.env.context.get('active_model') - if 'res_id' in fields and 'res_id' not in result: - result['res_id'] = self.env.context.get('active_id') - if 'reply_to_mode' in fields and 'reply_to_mode' not in result and result.get('model'): - # doesn't support threading + if 'res_ids' in fields_list and 'res_ids' not in result: + if self.env.context.get('active_ids'): + active_res_ids = self._parse_res_ids(self.env.context['active_ids']) + # beware, field is limited in storage, usage of active_ids in context still required + if active_res_ids and len(active_res_ids) <= self._batch_size: + result['res_ids'] = self.env.context['active_ids'] + elif self.env.context.get('active_id'): + result['res_ids'] = [self.env.context['active_id']] + else: + result['res_ids'] = False + # record / parent based computation + if result.get('composition_mode') == 'comment' and (set(fields_list) & {'model', 'res_ids', 'partner_ids', 'record_name', 'subject'}): + result.update(self.get_record_data(result)) + + # threading support check: 'update' requires to use message_update/post + if 'reply_to_mode' in fields_list and 'reply_to_mode' not in result and result.get('model'): if result['model'] not in self.env or not hasattr(self.env[result['model']], 'message_post'): result['reply_to_mode'] = 'new' - if 'active_domain' in self.env.context: # not context.get() because we want to keep global [] domains - result['active_domain'] = '%r' % self.env.context.get('active_domain') - if result.get('composition_mode') == 'comment' and (set(fields) & set(['model', 'res_id', 'partner_ids', 'record_name', 'subject'])): - result.update(self.get_record_data(result)) - # when being in new mode, create_uid is not granted -> ACLs issue may arise - if 'create_uid' in fields and 'create_uid' not in result: + if 'create_uid' in fields_list and 'create_uid' not in result: result['create_uid'] = self.env.uid - filtered_result = dict((fname, result[fname]) for fname in result if fname in fields) - return filtered_result + return { + fname: result[fname] + for fname in result if fname in fields_list + } def _partner_ids_domain(self): return expression.OR([ @@ -120,11 +134,15 @@ class MailComposer(models.TransientModel): selection=[('comment', 'Post on a document'), ('mass_mail', 'Email Mass Mailing')], string='Composition mode', default='comment') + composition_batch = fields.Boolean( + 'Batch composition', compute='_compute_composition_batch') # more than 1 record (raw source) model = fields.Char('Related Document Model') - res_id = fields.Integer('Related Document ID') + res_ids = fields.Text('Related Document IDs') + res_domain = fields.Text('Active domain') + res_domain_user_id = fields.Many2one( + 'res.users', string='Responsible', + help='Used as context used to evaluate composer domain') record_name = fields.Char('Message Record Name') - use_active_domain = fields.Boolean('Use active domain') - active_domain = fields.Text('Active domain', readonly=True) # characteristics message_type = fields.Selection([ ('comment', 'Comment'), @@ -158,6 +176,35 @@ class MailComposer(models.TransientModel): auto_delete_message = fields.Boolean('Delete Message Copy', help='Do not keep a copy of the email in the document communication history (mass mailing only)') mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server') + @api.constrains('res_ids') + def _check_res_ids(self): + """ Check res_ids is a valid list of integers (or Falsy). """ + for composer in self: + composer._evaluate_res_ids() + + @api.constrains('res_domain') + def _check_res_domain(self): + """ Check domain is a valid domain if set (otherwise it is considered + as a Falsy leaf. """ + for composer in self: + composer._evaluate_res_domain() + + @api.depends('res_ids') + def _compute_composition_batch(self): + """ Determine if batch mode is activated: + + * using res_domain: always batch (even if result is singleton at a + given time, it is user and time dependent, hence batch); + * res_ids: if more than one item in the list (void and singleton are + not batch); + """ + for composer in self: + if composer.res_domain: + composer.composition_batch = True + continue + res_ids = composer._evaluate_res_ids() + composer.composition_batch = len(res_ids) > 1 if res_ids else False + @api.depends('reply_to_force_new') def _compute_reply_to_mode(self): for composer in self: @@ -186,18 +233,19 @@ class MailComposer(models.TransientModel): @api.onchange('template_id') def _onchange_template_id_wrapper(self): self.ensure_one() - values = self._onchange_template_id(self.template_id.id, self.composition_mode, self.model, self.res_id)['value'] + values = self._onchange_template_id(self.template_id.id, self.composition_mode, self.model, self._evaluate_res_ids())['value'] for fname, value in values.items(): setattr(self, fname, value) - def _onchange_template_id(self, template_id, composition_mode, model, res_id): + def _onchange_template_id(self, template_id, composition_mode, model, res_ids): """ Perform the onchange. - * mass_mailing: we cannot render, so return the template values + * mass_mailing or comment in batch: we cannot render, so return the + template values * normal mode: return rendered values -> for x2many field, this onchange return command instead of ids """ - if template_id and composition_mode == 'mass_mail': + if template_id and (composition_mode == 'mass_mail' or len(res_ids) > 1): # copy raw template values (not rendered due to mass mode) template = self.env['mail.template'].browse(template_id) values = dict( @@ -214,11 +262,13 @@ class MailComposer(models.TransientModel): values['mail_server_id'] = template.mail_server_id.id if not tools.is_html_empty(template.body_html): values['body'] = template.body_html - elif template_id: + elif template_id and len(res_ids) <= 1: # render template (mono record, comment mode) and set it as composer values + # trick to evaluate qweb even when having no records + template_res_ids = res_ids if res_ids else [0] values = self._generate_template_for_composer( self.env['mail.template'].browse(template_id), - [res_id], + template_res_ids, ('attachment_ids', 'body_html', 'email_cc', @@ -230,7 +280,7 @@ class MailComposer(models.TransientModel): 'report_template', 'subject', ) - )[res_id] + )[template_res_ids[0]] # transform attachments into attachment_ids; not attached to the document because this will # be done further in the posting process, allowing to clean database if email not send attachment_ids = [] @@ -250,7 +300,7 @@ class MailComposer(models.TransientModel): default_values = self.with_context( default_composition_mode=composition_mode, default_model=model, - default_res_id=res_id + default_res_ids=res_ids ).default_get(['attachment_ids', 'body', 'composition_mode', @@ -260,7 +310,7 @@ class MailComposer(models.TransientModel): 'parent_id', 'partner_ids', 'reply_to', - 'res_id', + 'res_ids', 'subject', ]) values = dict( @@ -286,31 +336,33 @@ class MailComposer(models.TransientModel): a document (model, res_id). This is based on previously computed default values. """ result, record_name, subject = {}, False, False - model, res_id = values.get('model'), values.get('res_id') + model = values.get('model') + res_ids = self._parse_res_ids(values['res_ids']) if values.get('res_ids') else [] if values.get('parent_id'): parent = self.env['mail.message'].browse(values.get('parent_id')) if not model: model = parent.model result['model'] = model - if not res_id: - res_id = parent.res_id - result['res_id'] = res_id + if not res_ids: + res_ids = [parent.res_id] + result['res_ids'] = res_ids result['partner_ids'] = values.get('partner_ids', list()) + parent.partner_ids.ids record_name = parent.record_name subject = tools.ustr(parent.subject or '') - if (not subject or not record_name) and model and res_id: - record = self.env[model].browse(res_id) + if (not subject or not record_name) and model and res_ids and len(res_ids) == 1: + record = self.env[model].browse(res_ids[0]) if not record_name: record_name = record.display_name if not subject: subject = record._message_compute_subject() - elif model and res_id: - record = self.env[model].browse(res_id) + elif model and len(res_ids) == 1: + record = self.env[model].browse(res_ids[0]) record_name = record.display_name or '' subject = record._message_compute_subject() - result['record_name'] = record_name - result['subject'] = subject + if values.get('parent_id') or len(res_ids) == 1: # to be cleanup when moving to computed fields + result['record_name'] = record_name + result['subject'] = subject return result @@ -356,18 +408,14 @@ class MailComposer(models.TransientModel): result_mails_su, result_messages = self.env['mail.mail'].sudo(), self.env['mail.message'] for wizard in self: - # Mass Mailing - mass_mode = wizard.composition_mode == 'mass_mail' - - # wizard works in batch mode: [res_id] or active_ids or active_domain - if mass_mode and wizard.use_active_domain and wizard.model: - res_ids = self.env[wizard.model].search(ast.literal_eval(wizard.active_domain)).ids - elif mass_mode and wizard.model and self.env.context.get('active_ids'): - res_ids = self.env.context['active_ids'] + if wizard.res_domain: + search_domain = wizard._evaluate_res_domain() + search_user = wizard.res_domain_user_id or self.env.user + res_ids = self.env[wizard.model].with_user(search_user).search(search_domain).ids else: - res_ids = [wizard.res_id] if wizard.res_id else [] + res_ids = wizard._evaluate_res_ids() # in comment mode: raise here as anyway message_post will raise. - if not res_ids and not mass_mode: + if not res_ids and wizard.composition_mode == 'comment': raise ValueError( _('Mail composer in comment mode should run on at least one record. No records found (model %(model_name)s).', model_name=wizard.model) @@ -386,7 +434,7 @@ class MailComposer(models.TransientModel): self.ensure_one() post_values_all = self._prepare_mail_values(res_ids) ActiveModel = self.env[self.model] if self.model and hasattr(self.env[self.model], 'message_post') else self.env['mail.thread'] - if len(res_ids) > 1: + if self.composition_batch: # do not send emails directly but use the queue instead # add context key to avoid subscribing the author ActiveModel = ActiveModel.with_context( @@ -509,16 +557,17 @@ class MailComposer(models.TransientModel): """ self.ensure_one() email_mode = self.composition_mode == 'mass_mail' + rendering_mode = email_mode or self.composition_batch # values that do not depend on rendering mode base_values = self._prepare_mail_values_static() additional_values_all = {} # rendered based on raw content (wizard or template) - if email_mode and self.model: + if rendering_mode and self.model: additional_values_all = self._prepare_mail_values_dynamic(res_ids) # wizard content already rendered - elif not email_mode: + elif not rendering_mode: additional_values_all = self._prepare_mail_values_rendered(res_ids) mail_values_all = { @@ -607,17 +656,22 @@ class MailComposer(models.TransientModel): bodies = self._render_field( 'body', res_ids, # We want to preserve comments in emails so as to keep mso conditionals - options={'preserve_comments': self.composition_mode == 'mass_mail'}, + options={'preserve_comments': email_mode}, ) emails_from = self._render_field('email_from', res_ids) mail_values_all = { res_id: { 'body': bodies[res_id], # should be void - 'body_html': bodies[res_id] if email_mode else False, 'email_from': emails_from[res_id], - 'res_id': res_id, 'subject': subjects[res_id], + # some fields are specific to mail or message + **( + { + 'auto_delete': self.auto_delete, + 'body_html': bodies[res_id], + 'res_id': res_id, + } if email_mode else {}), } for res_id in res_ids } @@ -639,6 +693,9 @@ class MailComposer(models.TransientModel): for res_id in res_ids: # remove attachments from template values as they should not be rendered template_values[res_id].pop('attachment_ids', None) + # auto_delete is not supported in batch post, only in mass mail + if not email_mode: + template_values[res_id].pop('auto_delete', False) mail_values_all[res_id].update(template_values[res_id]) # Handle recipients. Without template, if no partner_ids is given, update @@ -674,25 +731,34 @@ class MailComposer(models.TransientModel): (name, base64.b64decode(enc_cont)) for name, enc_cont in mail_values.pop('attachments', []) ] - mail_values['attachment_ids'] = record._process_attachments_for_post( - decoded_attachments, - attachment_ids, - {'model': 'mail.message', 'res_id': 0} - )['attachment_ids'] + # email_mode: prepare processed attachments as commands for mail.mail + if email_mode: + mail_values['attachment_ids'] = record._process_attachments_for_post( + decoded_attachments, + attachment_ids, + {'model': 'mail.message', 'res_id': 0} + )['attachment_ids'] + # comment mode: prepare attachments as a list of IDs, to be processed by MailThread + else: + mail_values['attachments'] = decoded_attachments + mail_values['attachment_ids'] = attachment_ids # headers - mail_values['headers'] = repr(record._notify_by_email_get_headers()) + if email_mode: + mail_values['headers'] = repr(record._notify_by_email_get_headers()) # recipients: transform partner_ids (field used in mail_message) into # recipient_ids, used by mail_mail - recipient_ids_all = mail_values.pop('partner_ids', []) + self.partner_ids.ids - mail_values['recipient_ids'] = [(4, pid) for pid in recipient_ids_all] + if email_mode: + recipient_ids_all = mail_values.pop('partner_ids', []) + self.partner_ids.ids + mail_values['recipient_ids'] = [(4, pid) for pid in recipient_ids_all] # when having no specific reply_to -> fetch rendered email_from - reply_to = reply_to_values.get(res_id) - if not reply_to: - reply_to = mail_values.get('email_from', False) - mail_values['reply_to'] = reply_to + if email_mode: + reply_to = reply_to_values.get(res_id) + if not reply_to: + reply_to = mail_values.get('email_from', False) + mail_values['reply_to'] = reply_to return mail_values_all @@ -901,3 +967,63 @@ class MailComposer(models.TransientModel): ] } return recipients_info + + # ---------------------------------------------------------------------- + # MISC UTILS + # ---------------------------------------------------------------------- + + def _evaluate_res_domain(self): + """ Parse composer domain, which can be: an already valid list or + tuple (generally in code), a list or tuple as a string (coming from + actions). Void strings are considered as a falsy domain. + + :return: an Odoo domain (list of leaves) """ + self.ensure_one() + if isinstance(self.res_domain, (str, bool)) and not self.res_domain: + return expression.FALSE_DOMAIN + try: + domain = self.res_domain + if isinstance(self.res_domain, str): + domain = ast.literal_eval(domain) + + expression.expression( + domain, + self.env[self.model], + ) + except (ValueError, AssertionError) as e: + raise ValidationError( + _("Invalid domain %(domain)r (type %(domain_type)s)", + domain=self.res_domain, + domain_type=type(self.res_domain)) + ) from e + + return domain + + def _evaluate_res_ids(self): + """ Parse composer res_ids, which can be: an already valid list or + tuple (generally in code), a list or tuple as a string (coming from + actions). Void strings / missing values are evaluated as an empty list. + + Note that 'active_ids' context key is supported at this point as mailing + on big ID list would create issues if stored in database. + + :return: a list of IDs (empty list in case of falsy strings)""" + self.ensure_one() + return self._parse_res_ids( + self.res_ids or self.env.context.get('active_ids') + ) or [] + + @api.model + def _parse_res_ids(self, res_ids): + if tools.is_list_of(res_ids, int) or not res_ids: + return res_ids + error_msg = _("Invalid res_ids %(res_ids_str)s (type %(res_ids_type)s)", + res_ids_str=res_ids, + res_ids_type=type(res_ids)) + try: + res_ids = ast.literal_eval(res_ids) + except Exception as e: + raise ValidationError(error_msg) from e + if not tools.is_list_of(res_ids, int): + raise ValidationError(error_msg) + return res_ids diff --git a/addons/mail/wizard/mail_compose_message_views.xml b/addons/mail/wizard/mail_compose_message_views.xml index 8f7acb29202..6425d72410a 100644 --- a/addons/mail/wizard/mail_compose_message_views.xml +++ b/addons/mail/wizard/mail_compose_message_views.xml @@ -12,6 +12,7 @@ + @@ -21,7 +22,9 @@ - + + + new list @@ -440,7 +439,6 @@ new list diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 108e28d89f1..e1468fc3ba0 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -419,10 +419,7 @@ class PurchaseOrder(models.Model): ctx = dict(self.env.context or {}) ctx.update({ 'default_model': 'purchase.order', - 'active_model': 'purchase.order', - 'active_id': self.ids[0], - 'default_res_id': self.ids[0], - 'default_use_template': bool(template_id), + 'default_res_ids': self.ids, 'default_template_id': template_id, 'default_composition_mode': 'comment', 'default_email_layout_xmlid': "mail.mail_notification_layout_with_responsible_signature", @@ -781,10 +778,7 @@ class PurchaseOrder(models.Model): ctx = dict(self.env.context or {}) ctx.update({ 'default_model': 'purchase.order', - 'active_model': 'purchase.order', - 'active_id': self.ids[0], - 'default_res_id': self.ids[0], - 'default_use_template': bool(template_id), + 'default_res_ids': self.ids, 'default_template_id': template_id, 'default_composition_mode': 'comment', 'default_email_layout_xmlid': "mail.mail_notification_layout_with_responsible_signature", diff --git a/addons/repair/models/repair.py b/addons/repair/models/repair.py index 615e213a2b1..ef7452b97d5 100644 --- a/addons/repair/models/repair.py +++ b/addons/repair/models/repair.py @@ -332,8 +332,7 @@ class Repair(models.Model): template_id = self.env.ref('repair.mail_template_repair_quotation').id ctx = { 'default_model': 'repair.order', - 'default_res_id': self.id, - 'default_use_template': bool(template_id), + 'default_res_ids': self.ids, 'default_template_id': template_id, 'default_composition_mode': 'comment', 'default_email_layout_xmlid': 'mail.mail_notification_light', diff --git a/addons/sale/models/res_company.py b/addons/sale/models/res_company.py index 90609a9fd16..3380057638c 100644 --- a/addons/sale/models/res_company.py +++ b/addons/sale/models/res_company.py @@ -111,21 +111,20 @@ class ResCompany(models.Model): template = self.env.ref('sale.email_template_edi_sale', False) message_composer = self.env['mail.compose.message'].with_context( - default_use_template=bool(template), mark_so_as_sent=True, default_email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature', proforma=self.env.context.get('proforma', False), force_email=True, ).create({ - 'res_id': sample_sales_order.id, - 'template_id': template and template.id or False, - 'model': 'sale.order', - 'composition_mode': 'comment' + 'res_ids': sample_sales_order.ids, + 'template_id': template.id if template else False, + 'model': sample_sales_order._name, + 'composition_mode': 'comment', }) # Simulate the onchange (like trigger in form the view) update_values = message_composer._onchange_template_id( - template.id, 'comment', 'sale.order', sample_sales_order.id + template.id, 'comment', sample_sales_order._name, sample_sales_order.ids )['value'] message_composer.write(update_values) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index cc506c77b41..99375caa545 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -713,8 +713,7 @@ class SaleOrder(models.Model): lang = mail_template._render_lang(self.ids)[self.id] ctx = { 'default_model': 'sale.order', - 'default_res_id': self.id, - 'default_use_template': bool(mail_template), + 'default_res_ids': self.ids, 'default_template_id': mail_template.id if mail_template else None, 'default_composition_mode': 'comment', 'mark_so_as_sent': True, @@ -877,7 +876,6 @@ class SaleOrder(models.Model): if template.lang: lang = template._render_lang(self.ids)[self.id] ctx = { - 'default_use_template': bool(template_id), 'default_template_id': template_id, 'default_order_id': self.id, 'mark_so_as_canceled': True, diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index c6d2cdc26a1..7a8ec892ade 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -460,7 +460,6 @@ new list diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 1c701575a03..b75d5e16b1a 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -904,7 +904,6 @@ class Survey(models.Model): local_context = dict( self.env.context, default_survey_id=self.id, - default_use_template=bool(template), default_template_id=template and template.id or False, default_email_layout_xmlid='mail.mail_notification_light', default_send_email=(self.access_mode != 'public'), diff --git a/addons/test_mail/static/tests/activity_tests.js b/addons/test_mail/static/tests/activity_tests.js index 7ee548a2915..4e44d4326c9 100644 --- a/addons/test_mail/static/tests/activity_tests.js +++ b/addons/test_mail/static/tests/activity_tests.js @@ -350,10 +350,9 @@ QUnit.module("test_mail", {}, function () { assert.deepEqual( { default_model: "mail.test.activity", - default_res_id: mailTestActivityId2, + default_res_ids: [mailTestActivityId2], default_subtype_xmlid: 'mail.mt_comment', default_template_id: mailTemplateId1, - default_use_template: true, force_email: true, }, action.context diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index 8e0504ee260..f399f816e29 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from unittest.mock import patch +from ast import literal_eval +from unittest.mock import DEFAULT, patch from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.mail.wizard.mail_compose_message import MailComposer from odoo.addons.test_mail.models.test_mail_models import MailTestTicket from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients from odoo.exceptions import AccessError @@ -84,16 +86,16 @@ class TestMailComposer(TestMailCommon, TestRecipients): """ base_context = { 'default_model': records._name, + 'default_res_ids': records.ids, } if len(records) == 1: base_context['default_composition_mode'] = 'comment' - base_context['default_res_id'] = records.id else: base_context['default_composition_mode'] = 'mass_mail' - base_context['active_ids'] = records.ids if add_web: base_context['active_model'] = records._name base_context['active_id'] = records[0].id + base_context['active_ids'] = records.ids if values: base_context.update(**values) return base_context @@ -111,6 +113,7 @@ class TestComposerForm(TestMailComposer): self.assertFalse(composer_form.auto_delete_message) self.assertEqual(composer_form.author_id, self.env.user.partner_id) self.assertFalse(composer_form.body) + self.assertFalse(composer_form.composition_batch) self.assertEqual(composer_form.composition_mode, 'comment') self.assertEqual(composer_form.email_from, self.env.user.email_formatted) self.assertFalse(composer_form.mail_server_id) @@ -119,7 +122,7 @@ class TestComposerForm(TestMailComposer): self.assertEqual(composer_form.record_name, self.test_record.name, 'MailComposer: comment mode should compute record name') self.assertFalse(composer_form.reply_to) self.assertFalse(composer_form.reply_to_force_new) - self.assertEqual(composer_form.res_id, self.test_record.id) + self.assertEqual(literal_eval(composer_form.res_ids), self.test_record.ids) self.assertEqual(composer_form.subject, self.test_record._message_compute_subject()) self.assertIn(f'Ticket for {self.test_record.name}', composer_form.subject, 'Check effective content') @@ -187,6 +190,7 @@ class TestComposerForm(TestMailComposer): self.assertFalse(composer_form.auto_delete_message) self.assertEqual(composer_form.author_id, self.env.user.partner_id) self.assertEqual(composer_form.body, '

TemplateBody %s

' % self.test_record.name) + self.assertFalse(composer_form.composition_batch) self.assertEqual(composer_form.composition_mode, 'comment') self.assertEqual(composer_form.email_from, self.user_employee_2.email_formatted) self.assertEqual(composer_form.mail_server_id, self.mail_server_domain) @@ -195,10 +199,39 @@ class TestComposerForm(TestMailComposer): self.assertEqual(composer_form.record_name, self.test_record.name, 'MailComposer: comment mode should compute record name') self.assertEqual(composer_form.reply_to, 'info@test.example.com') self.assertFalse(composer_form.reply_to_force_new) - self.assertEqual(composer_form.res_id, self.test_record.id) + self.assertEqual(literal_eval(composer_form.res_ids), self.test_record.ids) self.assertEqual(composer_form.subject, 'TemplateSubject %s' % self.test_record.name) self.assertEqual(composer_form.subtype_id, self.env.ref('mail.mt_comment')) + @users('employee') + def test_mail_composer_comment_wtpl_batch(self): + """ Batch mode of composer in comment mode. """ + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context( + self.test_records, + add_web=True, + default_composition_mode='comment', + default_template_id=self.template.id), + )) + # self.assertTrue(composer_form.auto_delete) + self.assertFalse(composer_form.auto_delete) # FIXME: currently not taking template value + self.assertFalse(composer_form.auto_delete_message) + self.assertEqual(composer_form.author_id, self.env.user.partner_id) + self.assertEqual(composer_form.body, self.template.body_html, + 'MailComposer: comment in batch mode should have template raw body if template') + self.assertTrue(composer_form.composition_batch) + self.assertEqual(composer_form.composition_mode, 'comment') + self.assertEqual(composer_form.email_from, self.template.email_from, + 'MailComposer: comment in batch mode should have template raw email_from if template') + self.assertEqual(composer_form.mail_server_id, self.mail_server_domain) + self.assertEqual(composer_form.model, self.test_record._name) + self.assertFalse(composer_form.record_name, 'MailComposer: comment in batch mode should have void record name') + self.assertEqual(composer_form.reply_to, self.template.reply_to) + self.assertFalse(composer_form.reply_to_force_new) + self.assertEqual(literal_eval(composer_form.res_ids), self.test_records.ids) + self.assertEqual(composer_form.subject, self.template.subject, + 'MailComposer: comment in batch mode should have template raw subject if template') + @users('employee') def test_mail_composer_comment_wtpl_norecords(self): """ Test specific case when running without records, to see the rendering @@ -213,6 +246,7 @@ class TestComposerForm(TestMailComposer): self.assertFalse(composer_form.auto_delete_message) self.assertEqual(composer_form.author_id, self.env.user.partner_id) self.assertEqual(composer_form.body, '

TemplateBody

') + self.assertFalse(composer_form.composition_batch) self.assertEqual(composer_form.composition_mode, 'comment') self.assertEqual(composer_form.email_from, self.env.user.partner_id.email_formatted) self.assertEqual(composer_form.mail_server_id, self.mail_server_domain) @@ -221,7 +255,7 @@ class TestComposerForm(TestMailComposer): self.assertFalse(composer_form.record_name) self.assertEqual(composer_form.reply_to, 'info@test.example.com') self.assertFalse(composer_form.reply_to_force_new) - self.assertFalse(composer_form.res_id) + self.assertFalse(composer_form.res_ids) self.assertEqual(composer_form.subject, 'TemplateSubject ') self.assertEqual(composer_form.subtype_id, self.env.ref('mail.mt_comment')) @@ -234,6 +268,7 @@ class TestComposerForm(TestMailComposer): self.assertFalse(composer_form.auto_delete_message) self.assertEqual(composer_form.author_id, self.env.user.partner_id) self.assertFalse(composer_form.body) + self.assertTrue(composer_form.composition_batch) self.assertEqual(composer_form.composition_mode, 'mass_mail') self.assertEqual(composer_form.email_from, self.env.user.email_formatted) self.assertFalse(composer_form.mail_server_id) @@ -241,8 +276,7 @@ class TestComposerForm(TestMailComposer): self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name') self.assertFalse(composer_form.reply_to) self.assertFalse(composer_form.reply_to_force_new) - self.assertEqual(composer_form.res_id, self.test_records[0].id, - 'MailComposer: even in mass mode web active_id presence may add a res_id') + self.assertEqual(sorted(literal_eval(composer_form.res_ids)), sorted(self.test_records.ids)) self.assertFalse(composer_form.subject, 'MailComposer: mass mode should have void default subject if no template') self.assertEqual(composer_form.subtype_id, self.env.ref('mail.mt_comment')) @@ -257,6 +291,7 @@ class TestComposerForm(TestMailComposer): self.assertEqual(composer_form.author_id, self.env.user.partner_id) self.assertEqual(composer_form.body, self.template.body_html, 'MailComposer: mass mode should have template raw body if template') + self.assertTrue(composer_form.composition_batch) self.assertEqual(composer_form.composition_mode, 'mass_mail') self.assertEqual(composer_form.email_from, self.template.email_from, 'MailComposer: mass mode should have template raw email_from if template') @@ -265,8 +300,7 @@ class TestComposerForm(TestMailComposer): self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name') self.assertEqual(composer_form.reply_to, self.template.reply_to) self.assertFalse(composer_form.reply_to_force_new) - self.assertEqual(composer_form.res_id, self.test_records[0].id, - 'MailComposer: even in mass mode web active_id presence may add a res_id') + self.assertEqual(sorted(literal_eval(composer_form.res_ids)), sorted(self.test_records.ids)) self.assertEqual(composer_form.subject, self.template.subject, 'MailComposer: mass mode should have template raw subject if template') self.assertEqual(composer_form.subtype_id, self.env.ref('mail.mt_comment')) @@ -286,6 +320,7 @@ class TestComposerForm(TestMailComposer): self.assertEqual(composer_form.author_id, self.env.user.partner_id) self.assertEqual(composer_form.body, self.template.body_html, 'MailComposer: mass mode should have template raw body if template') + self.assertFalse(composer_form.composition_batch) self.assertEqual(composer_form.composition_mode, 'mass_mail') self.assertEqual(composer_form.email_from, self.template.email_from, 'MailComposer: mass mode should have template raw email_from if template') @@ -294,7 +329,7 @@ class TestComposerForm(TestMailComposer): self.assertFalse(composer_form.record_name, 'MailComposer: mass mode should have void record name') self.assertEqual(composer_form.reply_to, self.template.reply_to) self.assertFalse(composer_form.reply_to_force_new) - self.assertEqual(composer_form.res_id, 0) + self.assertFalse(composer_form.res_ids) self.assertEqual(composer_form.subject, self.template.subject, 'MailComposer: mass mode should have template raw subject if template') self.assertEqual(composer_form.subtype_id, self.env.ref('mail.mt_comment')) @@ -321,7 +356,8 @@ class TestComposerInternals(TestMailComposer): attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])]) self.assertEqual(len(attachs), 3) - for composition_mode, batch in (('comment', False), ('mass_mail', True)): + for composition_mode, batch in (('comment', False), ('comment', True), + ('mass_mail', False), ('mass_mail', True)): with self.subTest(composition_mode=composition_mode, batch=batch): test_records = self.test_records if batch else self.test_record ctx = self._get_web_context( @@ -337,7 +373,7 @@ class TestComposerInternals(TestMailComposer): composer._onchange_template_id_wrapper() # values coming from template: attachment_ids + report in comment - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(len(composer.attachment_ids), 4) for attach in attachs: self.assertIn(attach, composer.attachment_ids) @@ -358,7 +394,7 @@ class TestComposerInternals(TestMailComposer): # currently onchange necessary composer._onchange_template_id_wrapper() - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(composer.attachment_ids, attachs + generated, 'TODO: Values are kept (should be reset ?)') else: @@ -370,7 +406,7 @@ class TestComposerInternals(TestMailComposer): # currently onchange necessary composer._onchange_template_id_wrapper() - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(composer.attachment_ids, attachs + generated, 'TODO: Values are kept (should be reset ?)') else: @@ -386,7 +422,8 @@ class TestComposerInternals(TestMailComposer): 'email_from': False, }) - for composition_mode, batch in (('comment', False), ('mass_mail', True)): + for composition_mode, batch in (('comment', False), ('comment', True), + ('mass_mail', False), ('mass_mail', True)): with self.subTest(composition_mode=composition_mode, batch=batch): test_records = self.test_records if batch else self.test_record ctx = self._get_web_context( @@ -400,6 +437,7 @@ class TestComposerInternals(TestMailComposer): # default values are current user self.assertEqual(composer.author_id, self.env.user.partner_id) + self.assertEqual(composer.composition_mode, composition_mode) self.assertEqual(composer.email_from, self.env.user.email_formatted) # author values reset email (FIXME: currently not synchronized) @@ -414,12 +452,12 @@ class TestComposerInternals(TestMailComposer): composer._onchange_template_id_wrapper() self.assertEqual(composer.author_id, self.env.user.partner_id, 'MailComposer: should take value given by user') - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(composer.email_from, self.test_record.user_id.email_formatted, - 'MailComposer: should take email_from rendered from template') + f'MailComposer: should take email_from rendered from template ({composition_mode}-{batch})') else: self.assertEqual(composer.email_from, self.template.email_from, - 'MailComposer: should take email_from raw from template') + f'MailComposer: should take email_from raw from template ({composition_mode}-{batch})') # manual values are kept over template values composer.write({'email_from': self.test_from}) @@ -431,7 +469,7 @@ class TestComposerInternals(TestMailComposer): composer.write({'template_id': template_void.id}) # currently onchange necessary composer._onchange_template_id_wrapper() - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(composer.author_id, self.env.user.partner_id) self.assertEqual(composer.email_from, self.test_from) else: @@ -442,7 +480,7 @@ class TestComposerInternals(TestMailComposer): composer.write({'template_id': False}) # currently onchange necessary composer._onchange_template_id_wrapper() - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(composer.author_id, self.env.user.partner_id) self.assertEqual(composer.email_from, self.env.user.email_formatted) else: @@ -458,7 +496,8 @@ class TestComposerInternals(TestMailComposer): 'auto_delete': False, }) - for composition_mode, batch in (('comment', False), ('mass_mail', True)): + for composition_mode, batch in (('comment', False), ('comment', True), + ('mass_mail', False), ('mass_mail', True)): with self.subTest(composition_mode=composition_mode, batch=batch): test_records = self.test_records if batch else self.test_record ctx = self._get_web_context( @@ -539,7 +578,8 @@ class TestComposerInternals(TestMailComposer): 'subject': False, }) - for composition_mode, batch in (('comment', False), ('mass_mail', True)): + for composition_mode, batch in (('comment', False), ('comment', True), + ('mass_mail', False), ('mass_mail', True)): with self.subTest(composition_mode=composition_mode, batch=batch): test_records = self.test_records if batch else self.test_record ctx = self._get_web_context( @@ -557,11 +597,11 @@ class TestComposerInternals(TestMailComposer): # creation values are taken self.assertEqual(composer.body, '

Test Body />

') self.assertEqual(composer.mail_server_id, self.mail_server_global) self.assertEqual(composer.record_name, 'Manual update') @@ -618,7 +658,7 @@ class TestComposerInternals(TestMailComposer): # values are reset with default_get call, if it returns value # (aka subject for comment mode), and not record_name because # it was forgotten probably - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertFalse(composer.body) # self.assertFalse(composer.mail_server_id.id) self.assertEqual(composer.mail_server_id, self.mail_server_global, @@ -650,7 +690,7 @@ class TestComposerInternals(TestMailComposer): composer._onchange_template_id_wrapper() # values come from template - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(composer.body, f'

TemplateBody {self.test_record.name}

') self.assertEqual(composer.mail_server_id, self.template.mail_server_id) self.assertEqual(composer.record_name, self.test_record.name) @@ -670,7 +710,7 @@ class TestComposerInternals(TestMailComposer): composer._onchange_template_id_wrapper() # values come from template - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(composer.body, f'

TemplateBody {self.test_record.name}

') self.assertEqual(composer.mail_server_id, self.template.mail_server_id) self.assertEqual(composer.record_name, self.test_record.name) @@ -713,7 +753,8 @@ class TestComposerInternals(TestMailComposer): 'reply_to': False, }) - for composition_mode, batch in (('comment', False), ('mass_mail', True)): + for composition_mode, batch in (('comment', False), ('comment', True), + ('mass_mail', False), ('mass_mail', True)): with self.subTest(composition_mode=composition_mode, batch=batch): self.assertFalse( self.env['res.partner'].search([ @@ -769,7 +810,7 @@ class TestComposerInternals(TestMailComposer): ) # values come from template - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(len(new_partners), 2) self.assertEqual(composer.partner_ids, self.partner_1 + new_partners, 'Template took customer_id as set on record') self.assertEqual(composer.reply_to, 'info@test.example.com', 'Template was rendered') @@ -790,7 +831,7 @@ class TestComposerInternals(TestMailComposer): composer._onchange_template_id_wrapper() # values are kept, should probably be reset - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(composer.partner_ids, self.partner_admin, 'TODO: Values are kept (should be reset ?)') self.assertEqual(composer.reply_to, 'info@test.example.com', @@ -812,11 +853,11 @@ class TestComposerInternals(TestMailComposer): composer._onchange_template_id_wrapper() # values come from template - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(composer.partner_ids, self.partner_1 + new_partners) else: self.assertFalse(composer.partner_ids) - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(composer.reply_to, "info@test.example.com") else: self.assertEqual(composer.reply_to, self.template.reply_to) @@ -832,11 +873,11 @@ class TestComposerInternals(TestMailComposer): composer._onchange_template_id_wrapper() # values come from template - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(composer.partner_ids, self.partner_1 + new_partners) else: self.assertFalse(composer.partner_ids) - if composition_mode == 'comment': + if composition_mode == 'comment' and not batch: self.assertEqual(composer.reply_to, "info@test.example.com") else: self.assertEqual(composer.reply_to, self.template.reply_to) @@ -1137,10 +1178,11 @@ class TestComposerResultsComment(TestMailComposer): @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') def test_mail_composer_wtpl_complete(self): """ Test a posting process using a complex template, holding several - additional recipients and attachments. + additional recipients and attachments. It is done in monorecord and + in batch since this is now supported. This tests notifies: 2 new email_to (+ 1 duplicated), 1 email_cc, - test_record followers and partner_admin added in partner_to.""" + test_records followers and partner_admin added in partner_to.""" attachment_data = self._generate_attachments_data(2, self.template._name, self.template.id) email_to_1 = 'test.to.1@test.example.com' email_to_2 = 'test.to.2@test.example.com' @@ -1158,82 +1200,97 @@ class TestComposerResultsComment(TestMailComposer): attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])]) self.assertEqual(len(attachs), 2) - # ensure initial data - self.assertEqual(self.test_record.user_id, self.user_employee_2) - self.assertEqual(self.test_record.message_partner_ids, self.partner_employee_2) + for test_records, batch in ((self.test_record, False), (self.test_records, True)): + with self.subTest(test_records=test_records, batch=batch): + # ensure initial data + self.assertEqual(len(test_records.customer_id), len(test_records)) + self.assertEqual(test_records.user_id, self.user_employee_2) + self.assertEqual(test_records.message_partner_ids, self.partner_employee_2) - # open a composer and run it in comment mode - composer_form = Form(self.env['mail.compose.message'].with_context( - self._get_web_context(self.test_record, add_web=True, - default_template_id=self.template.id) - )) - composer = composer_form.save() - self.assertFalse(composer.reply_to_force_new, 'Mail: thread-enabled models should use auto thread by default') - with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app(): - composer._action_send_mail() + # open a composer and run it in comment mode + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(test_records, add_web=True, + default_composition_mode='comment', + default_template_id=self.template.id) + )) + composer = composer_form.save() + self.assertFalse(composer.reply_to_force_new, 'Mail: thread-enabled models should use auto thread by default') + with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app(): + composer._action_send_mail() - # check new partners have been created based on emails given - new_partners = self.env['res.partner'].search([ - ('email', 'in', [email_to_1, email_to_2, email_to_3, email_cc_1]) - ]) - self.assertEqual(len(new_partners), 3) - self.assertEqual(set(new_partners.mapped('email')), - set(['test.to.1@test.example.com', 'test.to.2@test.example.com', 'test.cc.1@test.example.com']) - ) + # check new partners have been created based on emails given + new_partners = self.env['res.partner'].search([ + ('email', 'in', [email_to_1, email_to_2, email_to_3, email_cc_1]) + ]) + self.assertEqual(len(new_partners), 3) + self.assertEqual(set(new_partners.mapped('email')), + set(['test.to.1@test.example.com', 'test.to.2@test.example.com', 'test.cc.1@test.example.com']) + ) - # global outgoing: one mail.mail (all customer recipients, then all employee recipients) - # and 5 emails, and 1 inbox notification (admin) - self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail') - self.assertEqual(len(self._mails), 5, 'Should have sent 5 emails, one per recipient') + # global outgoing: one mail.mail (all customer recipients, then all employee recipients) + # and 5 emails, and 1 inbox notification (admin) + self.assertEqual(len(self._new_mails), 2 * len(test_records), + 'Should have created 1 mail.mail per group per record') + # monorecord: force_send notifications + if not batch: + self.assertEqual(len(self._mails), 5, 'Should have sent 5 emails, one per recipient per record') + self.assertEqual(self._new_mails.mapped('state'), ['sent'] * len(self._new_mails)) + # mutlirecord: use email queue + else: + self.assertEqual(len(self._mails), 0, 'Should have put emails in queue and not sent any emails') + self.assertEqual(self._new_mails.mapped('state'), ['outgoing'] * len(self._new_mails)) + # simulate cron sending emails + self.env['mail.mail'].sudo().process_email_queue() - # template is sent only to partners (email_to are transformed) - message = self.test_record.message_ids[0] - self.assertMailMail(self.partner_employee_2, 'sent', - mail_message=message, - author=self.partner_employee, # author != email_from (template sets only email_from) - email_values={ - 'body_content': 'TemplateBody %s' % self.test_record.name, - 'email_from': self.test_record.user_id.email_formatted, # set by template - 'subject': 'TemplateSubject %s' % self.test_record.name, - 'attachments_info': [ - {'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'}, - {'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'}, - {'name': 'TestReport for %s.html' % self.test_record.name, 'type': 'text/plain'}, - ] - }, - fields_values={ - 'mail_server_id': self.mail_server_domain, - }, - ) - self.assertMailMail(self.test_record.customer_id + new_partners, 'sent', - mail_message=message, - author=self.partner_employee, # author != email_from (template sets only email_from) - email_values={ - 'body_content': 'TemplateBody %s' % self.test_record.name, - 'email_from': self.test_record.user_id.email_formatted, # set by template - 'subject': 'TemplateSubject %s' % self.test_record.name, - 'attachments_info': [ - {'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'}, - {'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'}, - {'name': 'TestReport for %s.html' % self.test_record.name, 'type': 'text/plain'}, - ] - }, - fields_values={ - 'mail_server_id': self.mail_server_domain, - }, - ) + # template is sent only to partners (email_to are transformed) + for test_record in test_records: + message = test_record.message_ids[0] + self.assertMailMail(self.partner_employee_2, 'sent', + mail_message=message, + author=self.partner_employee, # author != email_from (template sets only email_from) + email_values={ + 'body_content': f'TemplateBody {test_record.name}', + 'email_from': test_record.user_id.email_formatted, # set by template + 'subject': f'TemplateSubject {test_record.name}', + 'attachments_info': [ + {'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'}, + {'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'}, + {'name': f'TestReport for {test_record.name}.html', 'type': 'text/plain'}, + ] + }, + fields_values={ + 'mail_server_id': self.mail_server_domain, + }, + ) + self.assertMailMail(test_record.customer_id + new_partners, 'sent', + mail_message=message, + author=self.partner_employee, # author != email_from (template sets only email_from) + email_values={ + 'body_content': f'TemplateBody {test_record.name}', + 'email_from': test_record.user_id.email_formatted, # set by template + 'subject': f'TemplateSubject {test_record.name}', + 'attachments_info': [ + {'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'}, + {'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'}, + {'name': f'TestReport for {test_record.name}.html', 'type': 'text/plain'}, + ] + }, + fields_values={ + 'mail_server_id': self.mail_server_domain, + }, + ) - # message is posted and notified admin - self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment')) - self.assertNotified(message, [{'partner': self.partner_admin, 'is_read': False, 'type': 'inbox'}]) - # attachments are copied on message and linked to document - self.assertEqual( - set(message.attachment_ids.mapped('name')), - set(['AttFileName_00.txt', 'AttFileName_01.txt', 'TestReport for %s.html' % self.test_record.name]) - ) - self.assertEqual(set(message.attachment_ids.mapped('res_model')), set([self.test_record._name])) - self.assertEqual(set(message.attachment_ids.mapped('res_id')), set(self.test_record.ids)) - self.assertTrue(all(attach not in message.attachment_ids for attach in attachs), 'Should have copied attachments') + # message is posted and notified admin + self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment')) + self.assertNotified(message, [{'partner': self.partner_admin, 'is_read': False, 'type': 'inbox'}]) + # attachments are copied on message and linked to document + self.assertEqual( + set(message.attachment_ids.mapped('name')), + set(['AttFileName_00.txt', 'AttFileName_01.txt', f'TestReport for {test_record.name}.html']) + ) + self.assertEqual(set(message.attachment_ids.mapped('res_model')), set([test_record._name])) + self.assertEqual(set(message.attachment_ids.mapped('res_id')), set(test_record.ids)) + self.assertTrue(all(attach not in message.attachment_ids for attach in attachs), 'Should have copied attachments') @@ -1563,51 +1620,66 @@ class TestComposerResultsMass(TestMailComposer): @users('employee') @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') def test_mail_composer_wtpl_recipients(self): - """ Test various combinations of recipients: active_domain, active_id, + """ Test various combinations of recipients: res_domain, active_id, active_ids, ... to ensure fallback behavior are working. """ - # 1: active_domain + # 1: active ids composer_form = Form(self.env['mail.compose.message'].with_context( - self._get_web_context(self.test_records, add_web=True, - default_template_id=self.template.id, - active_ids=[], - default_use_active_domain=True, - default_active_domain=[('id', 'in', self.test_records.ids)]) + active_ids=self.test_records.ids, + default_composition_mode='mass_mail', + default_model=self.test_records._name, + default_template_id=self.template.id, )) composer = composer_form.save() + self.assertEqual(sorted(literal_eval(composer.res_ids)), sorted(self.test_records.ids)) + with self.mock_mail_gateway(mail_unlink_sent=True): composer._action_send_mail() + # should create emails in a single batch + self.assertEqual(self.build_email_mocked.call_count, 2, 'One build email per outgoing email') + self.assertEqual(self.mail_mail_create_mocked.call_count, 2, 'Emails are anyway created in a singleton loop') # global outgoing - self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') - self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record based on active_ids') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record based on on active_ids') for record in self.test_records: # template is sent directly using customer field, even if author is partner_employee self.assertSentEmail(self.partner_employee_2.email_formatted, record.customer_id) - # 2: active_domain not taken into account if use_active_domain is False + # 2: default_res_ids + active_ids -> res_ids takes lead composer_form = Form(self.env['mail.compose.message'].with_context( - self._get_web_context(self.test_records, add_web=True, + self._get_web_context(self.test_record, add_web=False, + default_composition_mode='mass_mail', + default_res_ids=self.test_record.ids, default_template_id=self.template.id, - default_use_active_domain=False, - default_active_domain=[('id', 'in', -1)]) + active_ids=self.test_records.ids, + ) )) composer = composer_form.save() + self.assertEqual(literal_eval(composer.res_ids), self.test_record.ids) + with self.mock_mail_gateway(mail_unlink_sent=True): composer._action_send_mail() # global outgoing - self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') - self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record') + self.assertEqual(len(self._new_mails), 1, 'Should have taken default_res_ids (1 record)') + self.assertEqual(len(self._mails), 1, 'Should have taken default_res_ids (1 record)') + + # template is sent directly using customer field, even if author is partner_employee + self.assertSentEmail(self.partner_employee_2.email_formatted, + self.test_record.customer_id) # 3: fallback on active_id if not active_ids composer_form = Form(self.env['mail.compose.message'].with_context( - self._get_web_context(self.test_records, add_web=True, - default_template_id=self.template.id, - active_ids=[]) + active_id=self.test_record.id, + default_composition_mode='mass_mail', + default_model=self.test_records._name, + default_template_id=self.template.id, )) composer = composer_form.save() + self.assertEqual(literal_eval(composer.res_ids), self.test_record.ids) + with self.mock_mail_gateway(mail_unlink_sent=False): composer._action_send_mail() @@ -1615,7 +1687,54 @@ class TestComposerResultsMass(TestMailComposer): self.assertEqual(len(self._new_mails), 1, 'Should have created 1 mail.mail per record') self.assertEqual(len(self._mails), 1, 'Should have sent 1 email per record') - # 3: void is void: raise in comment mode, just don't send anything in mass mail mode + # 4: _batch_size limit for active_ids + with patch.object(MailComposer, '_batch_size', new=1): + composer_form = Form(self.env['mail.compose.message'].with_context( + active_ids=self.test_records.ids, + default_composition_mode='mass_mail', + default_model=self.test_records._name, + default_template_id=self.template.id, + )) + composer = composer_form.save() + self.assertTrue(composer.composition_batch) + self.assertEqual(composer.composition_mode, 'mass_mail') + self.assertFalse(composer.res_ids) + + with self.mock_mail_gateway(mail_unlink_sent=True): + composer._action_send_mail() + + # should create emails in 2 batches of 1 + self.assertEqual(self.build_email_mocked.call_count, 2) + self.assertEqual(self.mail_mail_create_mocked.call_count, 2) + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record based on active_ids') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record based on on active_ids') + + # 5: mail.batch_size config parameter support, for sending only + self.env['ir.config_parameter'].sudo().set_param('mail.batch_size', 1) + with patch.object(MailComposer, '_batch_size', new=50): + composer_form = Form(self.env['mail.compose.message'].with_context( + active_ids=self.test_records.ids, + default_composition_mode='mass_mail', + default_model=self.test_records._name, + default_template_id=self.template.id, + )) + composer = composer_form.save() + self.assertTrue(composer.composition_batch) + self.assertEqual(composer.composition_mode, 'mass_mail') + self.assertEqual(sorted(literal_eval(composer.res_ids)), sorted(self.test_records.ids)) + + with self.mock_mail_gateway(mail_unlink_sent=True): + composer._action_send_mail() + + # should create emails in 2 batches of 1 + self.assertEqual(self.build_email_mocked.call_count, 2) + self.assertEqual(self.mail_mail_create_mocked.call_count, 2) + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record based on active_ids') + self.assertEqual(len(self._mails), 2, 'Should have sent 1 email per record based on on active_ids') + + # 6: void is void: raise in comment mode, just don't send anything in mass mail mode composer_form = Form(self.env['mail.compose.message'].with_context( default_model='mail.test.ticket', default_template_id=self.template.id @@ -1637,6 +1756,41 @@ class TestComposerResultsMass(TestMailComposer): composer._action_send_mail() self.assertNotSentEmail() + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_recipients_res_domain(self): + """ Test specific use case of res_domain usage, in combination with + res_domain_user_id allowing to set the evaluation environment for + the domain. """ + def _search_as_employee_2(recordset, *args, **kwargs): + """ Mock to return only one record, simulating a specific ir.rule """ + if recordset.env.uid == self.user_employee_2.id: + return self.test_records[0] + return DEFAULT + + composer_form = Form(self.env['mail.compose.message'].with_context( + default_composition_mode='mass_mail', + default_model=self.test_records._name, + default_res_domain=[('id', 'in', self.test_records.ids)], + default_res_domain_user_id=self.user_employee_2.id, + default_template_id=self.template.id, + )) + composer = composer_form.save() + self.assertEqual(literal_eval(composer.res_domain), [('id', 'in', self.test_records.ids)]) + self.assertEqual(composer.res_domain_user_id, self.user_employee_2) + + with self.mock_mail_gateway(mail_unlink_sent=True), \ + patch.object(MailTestTicket, 'search', autospec=True, side_effect=_search_as_employee_2): + composer._action_send_mail() + + # global outgoing + self.assertEqual(len(self._new_mails), 1, 'Should have created 1 mail.mail, search done by employee 2') + self.assertEqual(len(self._mails), 1, 'Should have sent 1 email, search done by employee 2') + + # template is sent directly using customer field, whatever the author + self.assertSentEmail(self.partner_employee_2.email_formatted, + self.test_records[0].customer_id) + @users('employee') @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') def test_mail_composer_wtpl_reply_to_force_new(self): diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index c3c98458cd3..a1ebb139e43 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -341,7 +341,7 @@ class TestMailAPIPerformance(BaseMailPerformance): composer = self.env['mail.compose.message'].with_context({ 'default_composition_mode': 'comment', 'default_model': test_record._name, - 'default_res_id': test_record.id, + 'default_res_ids': test_record.ids, }).create({ 'body': '

Test Body

', 'partner_ids': [(4, customer_id)], @@ -361,7 +361,7 @@ class TestMailAPIPerformance(BaseMailPerformance): composer = self.env['mail.compose.message'].with_context({ 'default_composition_mode': 'comment', 'default_model': test_record._name, - 'default_res_id': test_record.id, + 'default_res_ids': test_record.ids, }).create({ 'attachment_ids': attachments.ids, 'body': '

Test Body

', @@ -383,7 +383,7 @@ class TestMailAPIPerformance(BaseMailPerformance): self.env['mail.compose.message'].with_context({ 'default_composition_mode': 'comment', 'default_model': test_record._name, - 'default_res_id': test_record.id, + 'default_res_ids': test_record.ids, }) ) composer_form.body = '

Test Body

' @@ -408,9 +408,9 @@ class TestMailAPIPerformance(BaseMailPerformance): with self.assertQueryCount(__system__=3, employee=3): composer = self.env['mail.compose.message'].with_context({ - 'active_ids': test_records.ids, 'default_composition_mode': 'mass_mail', 'default_model': test_records._name, + 'default_res_ids': test_records.ids, 'default_template_id': test_template.id, }).create({}) composer._onchange_template_id_wrapper() @@ -430,7 +430,7 @@ class TestMailAPIPerformance(BaseMailPerformance): composer = self.env['mail.compose.message'].with_context({ 'default_composition_mode': 'comment', 'default_model': test_record._name, - 'default_res_id': test_record.id, + 'default_res_ids': test_record.ids, 'mail_auto_delete': False, }).create({ 'body': '

Test Body

', @@ -451,7 +451,7 @@ class TestMailAPIPerformance(BaseMailPerformance): composer = self.env['mail.compose.message'].with_context({ 'default_composition_mode': 'comment', 'default_model': test_record._name, - 'default_res_id': test_record.id, + 'default_res_ids': test_record.ids, 'default_template_id': test_template.id, }).create({}) composer._onchange_template_id_wrapper() @@ -476,7 +476,7 @@ class TestMailAPIPerformance(BaseMailPerformance): composer = self.env['mail.compose.message'].with_context({ 'default_composition_mode': 'comment', 'default_model': test_record._name, - 'default_res_id': test_record.id, + 'default_res_ids': test_record.ids, 'default_template_id': test_template.id, }).create({}) composer._onchange_template_id_wrapper() @@ -507,7 +507,7 @@ class TestMailAPIPerformance(BaseMailPerformance): self.env['mail.compose.message'].with_context({ 'default_composition_mode': 'comment', 'default_model': test_record._name, - 'default_res_id': test_record.id, + 'default_res_ids': test_record.ids, 'default_template_id': test_template.id, }) ) @@ -537,7 +537,7 @@ class TestMailAPIPerformance(BaseMailPerformance): self.env['mail.compose.message'].with_context({ 'default_composition_mode': 'comment', 'default_model': test_record._name, - 'default_res_id': test_record.id, + 'default_res_ids': test_record.ids, 'default_template_id': test_template.id, }) ) @@ -883,9 +883,9 @@ class TestMailComplexPerformance(BaseMailPerformance): with self.assertQueryCount(__system__=3, employee=3): composer = self.env['mail.compose.message'].with_context({ - 'active_ids': test_records.ids, 'default_composition_mode': 'mass_mail', 'default_model': test_records._name, + 'default_res_ids': test_records.ids, 'default_template_id': test_template.id, }).create({}) composer._onchange_template_id_wrapper() diff --git a/addons/test_mail_full/tests/test_portal.py b/addons/test_mail_full/tests/test_portal.py index 44a086a9901..a0f72c3d78a 100644 --- a/addons/test_mail_full/tests/test_portal.py +++ b/addons/test_mail_full/tests/test_portal.py @@ -218,9 +218,8 @@ class TestPortalFlow(MailCommon, HttpCase): 'default_composition_mode': 'comment', 'default_email_layout_xmlid': 'mail.mail_notification_layout_with_responsible_signature', 'default_model': self.record_portal._name, - 'default_res_id': self.record_portal.id, + 'default_res_ids': self.record_portal.ids, 'default_template_id': template_id, - 'default_use_template': template_id, 'force_email': True, 'lang': '{{ object.partner_id.lang }}', }) diff --git a/addons/website/models/website_visitor.py b/addons/website/models/website_visitor.py index 48be0b14f4b..27c61d775ac 100644 --- a/addons/website/models/website_visitor.py +++ b/addons/website/models/website_visitor.py @@ -167,7 +167,7 @@ class WebsiteVisitor(models.Model): def _prepare_message_composer_context(self): return { 'default_model': 'res.partner', - 'default_res_id': self.partner_id.id, + 'default_res_ids': self.partner_id.ids, 'default_partner_ids': [self.partner_id.id], } @@ -178,7 +178,6 @@ class WebsiteVisitor(models.Model): visitor_composer_ctx = self._prepare_message_composer_context() compose_form = self.env.ref('mail.email_compose_message_wizard_form', False) compose_ctx = dict( - default_use_template=False, default_composition_mode='comment', ) compose_ctx.update(**visitor_composer_ctx) diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index e6a805326c1..b70e34e747b 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -352,12 +352,10 @@ class SaleOrder(models.Model): 'context': { 'default_composition_mode': 'mass_mail' if len(self.ids) > 1 else 'comment', 'default_email_layout_xmlid': 'mail.mail_notification_layout_with_responsible_signature', - 'default_res_id': self.ids[0], + 'default_res_ids': self.ids, 'default_model': 'sale.order', - 'default_use_template': bool(template_id), 'default_template_id': template_id, 'website_sale_send_recovery_email': True, - 'active_ids': self.ids, }, } diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index 6b3a364d342..1399299fbdb 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -651,7 +651,6 @@ class Channel(models.Model): local_context = dict( self.env.context, default_channel_id=self.id, - default_use_template=bool(template), default_template_id=template and template.id or False, default_email_layout_xmlid='website_slides.mail_notification_channel_invite', )