From 7239115b4b8be76f5fe4b6f8d2734e9f5af37b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 16 Oct 2023 13:55:10 +0200 Subject: [PATCH] [IMP] mail: store company/alias environment on mail.message PURPOSE Allow alias domains to be multiple, notably to be used in a multi company environment where each company has its own alias domain. SPECIFICATIONS Store 'company_id' and 'alias_domain_id' on 'mail.message' model. It helps knowing the environment that produced the message, notably * for layouting: it will be used to improve company (and soon alias domain) given for the email notification layout; * for sending: it will be used to better compute mail-related values like default from, return path, ... When logging, those fields are kept false as anyway no notification is sent. No need to fetch extra information. Some sudo() are included in post_* methods, as portal may go through the posting method, see 'test_portal_acls' in test_message_post.py file. Task-36879 (Mail: Support Multi Domains Aliases) Part-of: odoo/odoo#76734 --- addons/mail/models/mail_message.py | 2 + addons/mail/models/mail_thread.py | 13 ++++ addons/mail/wizard/mail_compose_message.py | 45 +++++++++++++ .../wizard/mail_compose_message_views.xml | 2 + addons/test_mail/tests/test_mail_composer.py | 63 +++++++++++++++++-- 5 files changed, 119 insertions(+), 6 deletions(-) diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index bd99cfaa5e6..33bf3c25af9 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -111,6 +111,8 @@ class Message(models.Model): model = fields.Char('Related Document Model') res_id = fields.Many2oneReference('Related Document ID', model_field='model') record_name = fields.Char('Message Record Name') # display_name of the related document + record_alias_domain_id = fields.Many2one('mail.alias.domain', 'Alias Domain', ondelete='set null') + record_company_id = fields.Many2one('res.company', 'Company', ondelete='set null') # characteristics message_type = fields.Selection([ ('email', 'Email'), diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index e56c23f4578..2df2cc36a0e 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -2123,6 +2123,10 @@ class MailThread(models.AbstractModel): 'partner_ids': partner_ids, }) # add default-like values afterwards, to avoid useless queries + if 'record_alias_domain_id' not in msg_values: + msg_values['record_alias_domain_id'] = self.sudo()._mail_get_alias_domains(default_company=self.env.company)[self.id].id + if 'record_company_id' not in msg_values: + msg_values['record_company_id'] = self._mail_get_companies(default=self.env.company)[self.id].id if 'reply_to' not in msg_values: msg_values['reply_to'] = self._notify_get_reply_to(default=email_from)[self.id] @@ -2597,6 +2601,11 @@ class MailThread(models.AbstractModel): } msg_values.update(msg_kwargs) # add default-like values afterwards, to avoid useless queries + if self: + if 'record_alias_domain_id' not in msg_values: + msg_values['record_alias_domain_id'] = self._mail_get_alias_domains(default_company=self.env.company)[self.id].id + if 'record_company_id' not in msg_values: + msg_values['record_company_id'] = self._mail_get_companies(default=self.env.company)[self.id].id if 'reply_to' not in msg_values: msg_values['reply_to'] = self._notify_get_reply_to(default=email_from)[self.id if self else False] @@ -2689,6 +2698,8 @@ class MailThread(models.AbstractModel): 'email_from': email_from, # document 'model': self._name, + 'record_alias_domain_id': False, + 'record_company_id': False, 'record_name': False, # content 'attachment_ids': attachment_ids, @@ -2826,6 +2837,8 @@ class MailThread(models.AbstractModel): 'model', 'parent_id', 'partner_ids', + 'record_alias_domain_id', + 'record_company_id', 'record_name', 'reply_to', 'reply_to_force_new', diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 95798f552c1..4d5061c7551 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -121,6 +121,12 @@ class MailComposer(models.TransientModel): res_domain_user_id = fields.Many2one( 'res.users', string='Responsible', help='Used as context used to evaluate composer domain') + record_alias_domain_id = fields.Many2one( + 'mail.alias.domain', 'Alias Domain', + compute='_compute_record_environment', readonly=False, store=True) # useful only in monorecord comment mode + record_company_id = fields.Many2one( + 'res.company', 'Company', + compute='_compute_record_environment', readonly=False, store=True) # useful only in monorecord comment mode record_name = fields.Char( 'Record Name', compute='_compute_record_name', readonly=False, store=True) # useful only in monorecord comment mode @@ -387,6 +393,34 @@ class MailComposer(models.TransientModel): elif not active_res_ids and self.env.context.get('active_id'): composer.res_ids = f"{[self.env.context['active_id']]}" + @api.depends('composition_mode', 'model', 'res_domain', 'res_ids') + def _compute_record_environment(self): + """ In monorecord mode, fetch record company and the linked alias domain, + easing future processing notably at post and notification sending time. + + In batch mode it makes no sense to compute a single company, it will be + dynamically generated. """ + toreset = self.filtered( + lambda comp: (comp.record_company_id or comp.record_alias_domain_id) and comp.composition_batch + ) + if toreset: + toreset.record_alias_domain_id = False + toreset.record_company_id = False + + toupdate = self.filtered( + lambda comp: not comp.composition_batch + ) + for composer in toupdate: + res_ids = composer._evaluate_res_ids() + if composer.model and len(res_ids) == 1: + record = self.env[composer.model].browse(res_ids) + composer.record_company_id = record._mail_get_companies( + default=self.env.company + )[record.id] + composer.record_alias_domain_id = record._mail_get_alias_domains( + default_company=self.env.company + )[record.id] + @api.depends('composition_mode', 'model', 'parent_id', 'res_domain', 'res_ids') def _compute_record_name(self): """ Computation is coming either from parent message, either from the @@ -781,6 +815,8 @@ class MailComposer(models.TransientModel): STA - 'email_add_signature', STA - 'email_layout_xmlid', DYN - 'force_email_lang', # notify parameter + STA - 'record_alias_domain_id', # monorecord only + STA - 'record_company_id', # monorecord only BOTH DYN - 'attachment_ids', @@ -879,6 +915,8 @@ class MailComposer(models.TransientModel): force_send=self.force_send, mail_auto_delete=self.auto_delete, model_description=model_description, + record_alias_domain_id=self.record_alias_domain_id.id, + record_company_id=self.record_company_id.id, ) return values @@ -899,6 +937,10 @@ class MailComposer(models.TransientModel): RecordsModel = self.env[self.model].with_prefetch(res_ids) email_mode = self.composition_mode == 'mass_mail' + # records values + companies = RecordsModel.browse(res_ids)._mail_get_companies(default=self.env.company) + alias_domains = RecordsModel.browse(res_ids)._mail_get_alias_domains(default_company=self.env.company) + # langs, used currently only to propagate in comment mode for notification # layout translation langs = self._render_field('lang', res_ids) @@ -916,6 +958,9 @@ class MailComposer(models.TransientModel): 'email_from': emails_from[res_id], 'scheduled_date': False, 'subject': subjects[res_id], + # record-specific environment values (company, alias_domain) + 'record_alias_domain_id': alias_domains[res_id].id, + 'record_company_id': companies[res_id].id, # some fields are specific to mail **( { diff --git a/addons/mail/wizard/mail_compose_message_views.xml b/addons/mail/wizard/mail_compose_message_views.xml index 7d59093065f..3370d02acf7 100644 --- a/addons/mail/wizard/mail_compose_message_views.xml +++ b/addons/mail/wizard/mail_compose_message_views.xml @@ -21,6 +21,8 @@ + + diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index 88769e7f0c8..3becd823fad 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -171,6 +171,8 @@ class TestComposerForm(TestMailComposer): self.assertFalse(composer_form.mail_server_id) self.assertEqual(composer_form.model, self.test_record._name) self.assertFalse(composer_form.partner_ids) + self.assertEqual(composer_form.record_alias_domain_id, self.mail_alias_domain) + self.assertEqual(composer_form.record_company_id, self.env.company) 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) @@ -182,6 +184,32 @@ class TestComposerForm(TestMailComposer): self.assertEqual(composer_form.subtype_id, self.env.ref('mail.mt_comment')) self.assertFalse(composer_form.subtype_is_log) + @users('employee') + def test_mail_composer_comment_mc(self): + """ Test values specific to multi-company setup, in comment mode, using + the Form tool. """ + # add access to second company to avoid MC rules on ticket model + self.env.user.company_ids = [(4, self.company_2.id)] + test_record = self.test_record.with_env(self.env) + source_company = [ + self.company_admin, + self.company_2, + self.env['res.company'], + ] + expected = [ + (self.company_admin, self.mail_alias_domain), + (self.company_2, self.mail_alias_domain_c2), + (self.company_admin, self.mail_alias_domain), # env company + ] + for company, (exp_company, exp_alias_domain) in zip(source_company, expected): + with self.subTest(cmopany=company): + test_record.write({'company_id': company.id}) + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=True) + )) + self.assertEqual(composer_form.record_alias_domain_id, exp_alias_domain) + self.assertEqual(composer_form.record_company_id, exp_company) + @users('employee') def test_mail_composer_comment_attachments(self): """Tests that all attachments are added to the composer, static attachments @@ -251,6 +279,8 @@ class TestComposerForm(TestMailComposer): self.assertEqual(composer_form.mail_server_id, self.mail_server_domain) self.assertEqual(composer_form.model, self.test_record._name) self.assertEqual(composer_form.partner_ids[:], self.partner_1) + self.assertEqual(composer_form.record_alias_domain_id, self.mail_alias_domain) + self.assertEqual(composer_form.record_company_id, self.env.company) 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) @@ -283,6 +313,8 @@ class TestComposerForm(TestMailComposer): self.assertFalse(composer_form.force_send, 'MailComposer: batch record post use email queue for notifications') 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_alias_domain_id, 'MailComposer: comment in batch mode should have void alias domain') + self.assertFalse(composer_form.record_company_id, 'MailComposer: comment in batch mode should have void company') 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) @@ -315,6 +347,8 @@ class TestComposerForm(TestMailComposer): self.assertFalse(composer_form.force_send, 'MailComposer: batch record post use email queue for notifications') 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_alias_domain_id, 'MailComposer: comment in batch mode should have void alias domain') + self.assertFalse(composer_form.record_company_id, 'MailComposer: comment in batch mode should have void company') 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) @@ -347,6 +381,8 @@ class TestComposerForm(TestMailComposer): self.assertEqual(composer_form.mail_server_id, self.mail_server_domain) self.assertEqual(composer_form.model, self.test_record._name) self.assertFalse(composer_form.partner_ids[:]) + self.assertFalse(composer_form.record_alias_domain_id) + self.assertFalse(composer_form.record_company_id) self.assertFalse(composer_form.record_name) self.assertEqual(composer_form.reply_to, 'info@test.example.com') self.assertFalse(composer_form.reply_to_force_new) @@ -375,6 +411,8 @@ class TestComposerForm(TestMailComposer): self.assertTrue(composer_form.force_send, 'MailComposer: mass mode sends emails right away') self.assertFalse(composer_form.mail_server_id) self.assertEqual(composer_form.model, self.test_records._name) + self.assertFalse(composer_form.record_alias_domain_id, 'MailComposer: mass mode should have void alias domain') + self.assertFalse(composer_form.record_company_id, 'MailComposer: mass mode should have void company') 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) @@ -404,6 +442,8 @@ class TestComposerForm(TestMailComposer): self.assertTrue(composer_form.force_send, 'MailComposer: mass mode sends emails right away') self.assertEqual(composer_form.mail_server_id, self.mail_server_domain) self.assertEqual(composer_form.model, self.test_records._name) + self.assertFalse(composer_form.record_alias_domain_id, 'MailComposer: mass mode should have void alias domain') + self.assertFalse(composer_form.record_company_id, 'MailComposer: mass mode should have void company') 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) @@ -437,6 +477,8 @@ class TestComposerForm(TestMailComposer): self.assertTrue(composer_form.force_send, 'MailComposer: mass mode sends emails right away') self.assertEqual(composer_form.mail_server_id, self.mail_server_domain) self.assertEqual(composer_form.model, self.test_records._name) + self.assertFalse(composer_form.record_alias_domain_id, 'MailComposer: mass mode should have void alias domain') + self.assertFalse(composer_form.record_company_id, 'MailComposer: mass mode should have void company') 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) @@ -470,6 +512,8 @@ class TestComposerForm(TestMailComposer): self.assertTrue(composer_form.force_send, 'MailComposer: mass mode sends emails right away') self.assertEqual(composer_form.mail_server_id, self.mail_server_domain) self.assertEqual(composer_form.model, self.test_records._name) + self.assertFalse(composer_form.record_alias_domain_id, 'MailComposer: mass mode should have void alias domain') + self.assertFalse(composer_form.record_company_id, 'MailComposer: mass mode should have void company') 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) @@ -1262,6 +1306,12 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): """ Ensure class initial data to ease understanding """ self.assertTrue(self.template.auto_delete) + self.assertEqual(len(self.test_record), 1) + self.assertEqual(self.test_record.user_id, self.user_employee_2) + self.assertEqual(self.test_record.message_partner_ids, self.partner_employee_2) + self.assertEqual(self.test_record[0].customer_id.lang, 'en_US') + self.assertEqual(self.test_record.company_id, self.company_admin) + self.assertEqual(len(self.test_records), 2) self.assertEqual(self.test_records.user_id, self.user_employee_2) self.assertEqual(self.test_records.message_partner_ids, self.partner_employee_2) @@ -1844,7 +1894,6 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): 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') - @users('employee') @mute_logger('odoo.addons.mail.models.mail_mail') def test_mail_composer_wtpl_mc(self): @@ -1871,11 +1920,11 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])]) self.assertEqual(len(attachs), 2) - for batch, companies, expected_companies in [ + for batch, companies, expected_companies, expected_alias_domains in [ + (False, self.company_admin, self.company_admin, self.mail_alias_domain), ( - False, self.company_admin, self.company_admin - ), ( True, self.company_admin + self.company_2, self.company_admin + self.company_2, + self.mail_alias_domain + self.mail_alias_domain_c2, ), ]: with self.subTest(batch=batch, @@ -1902,8 +1951,8 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): new_partner = self.env['res.partner'].search([('email_normalized', '=', 'test.to.1@test.example.com')]) self.assertEqual(len(new_partner), 1) # check output, company-specific values mainly for this test - for record, exp_company in zip( - test_records, expected_companies + for record, exp_company, exp_alias_domain in zip( + test_records, expected_companies, expected_alias_domains ): message = record.message_ids[0] for recipient in [self.partner_employee_2, new_partner, record.customer_id]: @@ -1924,6 +1973,8 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): 'X-Odoo-Objects': f'{record._name}-{record.id}', }, 'mail_server_id': self.env['ir.mail_server'], + 'record_alias_domain_id': exp_alias_domain, + 'record_company_id': exp_company, 'subject': f'TemplateSubject {record.name}', }, )