diff --git a/addons/test_mail/models/test_mail_models.py b/addons/test_mail/models/test_mail_models.py index d479cea1ce0..460faaf4584 100644 --- a/addons/test_mail/models/test_mail_models.py +++ b/addons/test_mail/models/test_mail_models.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ from odoo.tools import email_normalize -from odoo.tools.translate import _ class MailTestSimple(models.Model): @@ -164,10 +163,22 @@ class MailTestTicket(models.Model): def _notify_get_recipients_groups(self, msg_vals=None): """ Activate more groups to test query counters notably (and be backward compatible for tests). """ - groups = super(MailTestTicket, self)._notify_get_recipients_groups(msg_vals=msg_vals) + local_msg_vals = dict(msg_vals or {}) + groups = super()._notify_get_recipients_groups(msg_vals=msg_vals) for group_name, _group_method, group_data in groups: if group_name == 'portal': group_data['active'] = True + elif group_name == 'customer': + group_data['active'] = True + group_data['has_button_access'] = True + group_data['actions'] = [{ + 'url': self._notify_get_action_link( + 'controller', + controller='/test_mail/do_stuff', + **local_msg_vals + ), + 'title': _('NotificationButtonTitle') + }] return groups diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index a399ee0521a..132c23aaa54 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -89,6 +89,7 @@ class TestMailComposer(TestMailCommon, TestRecipients): 'partner_to': '{{ object.customer_id.id if object.customer_id else "" }}', 'email_to': '{{ (object.email_from if not object.customer_id else "") }}', 'email_from': '{{ (object.user_id.email_formatted or user.email_formatted) }}', + 'lang': '{{ object.customer_id.lang }}', 'mail_server_id': cls.mail_server_domain.id, 'model_id': cls.env['ir.model']._get('mail.test.ticket').id, 'reply_to': '{{ ctx.get("custom_reply_to") or "info@test.example.com" }}', @@ -129,6 +130,14 @@ class TestMailComposer(TestMailCommon, TestRecipients): @tagged('mail_composer') class TestComposerForm(TestMailComposer): + def test_assert_initial_data(self): + """ Ensure class initial data to ease understanding """ + self.assertTrue(self.template.auto_delete) + + 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) + @users('employee') def test_mail_composer_comment(self): composer_form = Form(self.env['mail.compose.message'].with_context( @@ -1153,11 +1162,26 @@ class TestComposerInternals(TestMailComposer): self.assertEqual(template.body_html, '

Template Body

', 'email_template incorrect body_html') -@tagged('mail_composer') +@tagged('mail_composer', 'multi_lang') class TestComposerResultsComment(TestMailComposer, CronMixinCase): """ Test global output of composer used in comment mode. Test notably notification and emails generated during this process. """ + def test_assert_initial_data(self): + """ Ensure class initial data to ease understanding """ + self.assertTrue(self.template.auto_delete) + + 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) + self.assertEqual(self.test_records[0].customer_id.lang, 'en_US') + self.assertEqual(self.test_records[1].customer_id.lang, 'en_US') + + self.assertEqual(len(self.test_partners), 2) + + self.assertEqual(self.user_employee.lang, 'en_US') + self.assertEqual(self.user_employee_2.lang, 'en_US') + @users('employee') def test_mail_composer_default_subject(self): """ Make sure the default subject is applied in the composer. """ @@ -1322,6 +1346,10 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): * scheduled_date: creates mail.message.schedule (no email sent), then scheduling send notifications with notification parameters kept * otherwise: global behavior + + Test with and without notification layout specified. + + Test with and without languages. """ attachment_data = self._generate_attachments_data(2, self.template._name, self.template.id) email_to_1 = 'test.to.1@test.example.com' @@ -1339,17 +1367,37 @@ 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_mode, scheduled_date, email_layout_xmlid in product( + for batch_mode, scheduled_date, email_layout_xmlid, use_lang in product( (False, True, 'domain'), (False, '{{ (object.create_date or datetime.datetime(2022, 12, 26, 18, 0, 0)) + datetime.timedelta(days=2) }}'), (False, 'mail.test_layout'), + (False, True), ): - with self.subTest(batch_mode=batch_mode, scheduled_date=scheduled_date): + with self.subTest(batch_mode=batch_mode, + scheduled_date=scheduled_date, + email_layout_xmlid=email_layout_xmlid, + use_lang=use_lang): + # update test configuration batch = bool(batch_mode) self.template.write({ 'scheduled_date': scheduled_date, 'email_layout_xmlid': email_layout_xmlid, }) + if use_lang: + if batch: + langs = ('es_ES', 'en_US') + self.test_partners[0].lang = langs[0] + self.test_partners[1].lang = langs[1] + else: + langs = ('es_ES',) + self.partner_1.lang = langs[0] + if not use_lang: + if batch: + langs = (False, False) + self.test_partners.lang = False + else: + langs = (False,) + self.partner_1.lang = False test_records = self.test_records if batch else self.test_record # ensure initial data @@ -1383,7 +1431,7 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): else: author = self.partner_employee_2 self.assertEqual(composer.author_id, author, - 'Author cannot be synchronized with a raw email_from') + 'Author is synchronized with rendered email_from') self.assertEqual(composer.email_from, self.partner_employee_2.email_formatted) self.assertFalse(composer.reply_to_force_new, 'Mail: thread-enabled models should use auto thread by default') @@ -1426,7 +1474,11 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): 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']) + {'test.to.1@test.example.com', 'test.to.2@test.example.com', 'test.cc.1@test.example.com'}, + ) + self.assertEqual( + set(new_partners.mapped('lang')), + {'en_US'}, ) # if scheduled_date is set: simulate cron for sending notifications @@ -1453,15 +1505,31 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): self.env['mail.mail'].sudo().process_email_queue() # template is sent only to partners (email_to are transformed) - for test_record in test_records: + for test_record, exp_lang in zip(test_records, langs): message = test_record.message_ids[0] + + # check created mail.mail and outgoing emails. In comment + # 2 mails are generated (due to group-based layouting): + # - one for recipient that is a user + # - one for recipients that are customers + # Then each recipient receives its own outging email. See + # 'assertMailMail' for more details. + + # TDE FIXME: lang not supported in batch mode, translations + # are fetched on composer-side, not template-side + if exp_lang == 'es_ES' and not batch: + exp_body = f'SpanishBody for {test_record.name}' + exp_subject = f'SpanishSubject for {test_record.name}' + else: + exp_body = f'TemplateBody {test_record.name}' + exp_subject = f'TemplateSubject {test_record.name}' self.assertMailMail(self.partner_employee_2, 'sent', mail_message=message, author=author, # author is different in batch and monorecord mode (raw or rendered email_from) email_values={ - 'body_content': f'TemplateBody {test_record.name}', + 'body_content': exp_body, 'email_from': test_record.user_id.email_formatted, # set by template - 'subject': f'TemplateSubject {test_record.name}', + 'subject': exp_subject, 'attachments_info': [ {'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'}, {'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'}, @@ -1477,9 +1545,9 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): mail_message=message, author=author, # author is different in batch and monorecord mode (raw or rendered email_from) email_values={ - 'body_content': f'TemplateBody {test_record.name}', + 'body_content': exp_body, 'email_from': test_record.user_id.email_formatted, # set by template - 'subject': f'TemplateSubject {test_record.name}', + 'subject': exp_subject, 'attachments_info': [ {'name': 'AttFileName_00.txt', 'raw': b'AttContent_00', 'type': 'text/plain'}, {'name': 'AttFileName_01.txt', 'raw': b'AttContent_01', 'type': 'text/plain'}, @@ -1492,6 +1560,36 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): }, ) + # Low-level checks on outgoing email for the recipient to + # check layouting and language. Note that standard layout + # is not tested against translations, only the custom one + # to ease translations checks. + email = self._find_sent_email(test_record.user_id.email_formatted, [test_record.customer_id.email_formatted]) + self.assertTrue(bool(email), 'Email not found, check recipients') + + # TDE FIXME: as it currently depends on a context-based hack + # translation is not supported when scheduling notifications + # or when a domain is given. Moreover access buttons are not + # translated + exp_layout_content_en = 'English Layout for Ticket-like model' + exp_layout_content_es = 'Spanish Layout para Spanish Model Description' + exp_button_en = 'View Ticket-like model' + # exp_button_es = 'SpanishView Spanish Model Description' + if email_layout_xmlid: + if exp_lang == 'es_ES' and batch_mode != 'domain' and not scheduled_date: + self.assertIn(exp_layout_content_es, email['body']) + self.assertIn(exp_button_en, email['body'], + 'TODO: buttons should be translated') + else: + self.assertIn(exp_layout_content_en, email['body']) + self.assertIn(exp_button_en, email['body']) + else: + # check default layouting applies + if exp_lang == 'es_ES' and batch_mode != 'domain' and not scheduled_date: + self.assertIn('html lang="es_ES"', email['body']) + else: + self.assertIn('html lang="en_US"', email['body']) + # 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'}]) @@ -1560,7 +1658,7 @@ class TestComposerResultsCommentStatus(TestMailComposer): 'model_id': cls.env['ir.model']._get_id(cls.test_records._name), }) - def test_initial_data(self): + def test_assert_initial_data(self): """ Ensure class initial data to ease understanding """ self.assertFalse(self.template.auto_delete) @@ -1603,7 +1701,7 @@ class TestComposerResultsCommentStatus(TestMailComposer): self.assertEqual(len(self._mails), 2, 'Should have sent 2 emails, skipping the exclusion list') -@tagged('mail_composer') +@tagged('mail_composer', 'multi_lang') class TestComposerResultsMass(TestMailComposer): @classmethod @@ -1718,7 +1816,10 @@ class TestComposerResultsMass(TestMailComposer): @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') def test_mail_composer_wtpl_complete(self): """ Test a composer in mass mode with a quite complete template, containing - notably email-based recipients and attachments. """ + notably email-based recipients and attachments. + + Translations and email layout supported are also tested. + """ # as we use the email queue, don't have failing tests due to other outgoing emails self.env['mail.mail'].sudo().search([]).unlink() @@ -1742,109 +1843,155 @@ class TestComposerResultsMass(TestMailComposer): self.assertEqual(self.test_records.user_id, self.user_employee_2) self.assertEqual(self.test_records.message_partner_ids, self.partner_employee_2) - # launch composer in mass mode - 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) - )) - composer = composer_form.save() - # ensure some parameters used afterwards - author = self.env.user.partner_id - self.assertEqual(composer.author_id, author, - 'Author cannot be synchronized with a raw email_from') - self.assertEqual(composer.email_from, self.template.email_from) + for use_domain, scheduled_date, email_layout_xmlid, use_lang in product( + (False, True), + (False, '{{ (object.create_date or datetime.datetime(2022, 12, 26, 18, 0, 0)) + datetime.timedelta(days=2) }}'), + (False, 'mail.test_layout'), + (False, True), + ): + with self.subTest(use_domain=use_domain, + scheduled_date=scheduled_date, + email_layout_xmlid=email_layout_xmlid, + use_lang=use_lang): + # update test configuration + self.template.write({ + 'scheduled_date': scheduled_date, + }) + if use_lang: + langs = ('es_ES', 'en_US') + self.test_partners[0].lang = langs[0] + self.test_partners[1].lang = langs[1] + else: + langs = (False, False) + self.test_partners.lang = False - with self.mock_mail_gateway(mail_unlink_sent=False), \ - freeze_time(self.reference_now): - composer._action_send_mail() + ctx = { + 'default_model': self.test_records._name, + 'default_composition_mode': 'mass_mail', + 'default_template_id': self.template.id, + } + if use_domain: + ctx['default_res_domain'] = [('id', 'in', self.test_records.ids)] + else: + ctx['default_res_ids'] = self.test_records.ids + if email_layout_xmlid: + ctx['default_email_layout_xmlid'] = email_layout_xmlid - 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) + # launch composer in mass mode + composer_form = Form(self.env['mail.compose.message'].with_context(ctx)) + composer = composer_form.save() - # global outgoing: emails not sent due to scheduled_date - self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') - self.assertEqual(len(self._mails), 0, 'Should not send emails, scheduled in the future') - self.assertEqual(self._new_mails.mapped('scheduled_date'), - [self.reference_now + timedelta(days=2)] * 2) + # ensure some parameters used afterwards + author = self.env.user.partner_id + self.assertEqual(composer.author_id, author, + 'Author is not synchronized, as template email_from does not match existing partner') + self.assertEqual(composer.email_from, self.template.email_from) - # simulate cron queue at right time for sending - with freeze_time(self.reference_now + timedelta(days=2)): - self.env['mail.mail'].sudo().process_email_queue() + with self.mock_mail_gateway(mail_unlink_sent=False), \ + freeze_time(self.reference_now): + composer._action_send_mail() - self.assertEqual(len(self._mails), 10, 'Should have sent 5 emails per record') + # partners created from raw emails + 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(new_partners.mapped('lang'), ['en_US'] * 3, + 'New partners lang is always the default DB one, whatever the context') - # hack to use assertEmails: filtering on from/to only is not sufficient to distinguish emails - _mails_records = [ - [mail for mail in self._mails if '%s-%s' % (record.id, record._name) in mail['message_id']] - for record in self.test_records - ] + # check global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + if not scheduled_date: + # emails sent directly + self.assertEqual(len(self._mails), 10, 'Should have sent emails') + self.assertEqual(self._new_mails.mapped('scheduled_date'), + [False] * 2) + else: + # emails not sent due to scheduled_date + self.assertEqual(len(self._mails), 0, 'Should not send emails, scheduled in the future') + self.assertEqual(self._new_mails.mapped('scheduled_date'), + [self.reference_now + timedelta(days=2)] * 2) - for record, _mails in zip(self.test_records, _mails_records): - # message copy is kept - message = record.message_ids[0] + # simulate cron queue at right time for sending + with freeze_time(self.reference_now + timedelta(days=2)): + self.env['mail.mail'].sudo().process_email_queue() - # template is sent only to partners (email_to are transformed) - self._mails = _mails - self.assertMailMail(record.customer_id + new_partners + self.partner_admin, - 'sent', - mail_message=message, - author=author, - email_values={ - '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' % record.name, 'type': 'text/plain'}, - ], - 'body_content': 'TemplateBody %s' % record.name, - 'email_from': self.partner_employee_2.email_formatted, - 'subject': 'TemplateSubject %s' % record.name, - }, - fields_values={ - 'email_from': self.partner_employee_2.email_formatted, - 'mail_server_id': self.mail_server_domain, - 'reply_to': formataddr(( - f'{self.env.user.company_id.name} {record.name}', - f'{self.alias_catchall}@{self.alias_domain}' - )), - 'subject': 'TemplateSubject %s' % record.name, - }, - ) + # everything should be sent now + self.assertEqual(len(self._mails), 10, 'Should have sent 5 emails per record') - # test without catchall filling reply-to - 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) - )) - composer = composer_form.save() - with self.mock_mail_gateway(mail_unlink_sent=True): - # remove alias so that _notify_get_reply_to will return the default value instead of alias - self.env['ir.config_parameter'].sudo().set_param("mail.catchall.domain", None) - composer.action_send_mail() + # check email content + for record, exp_lang in zip(self.test_records, langs): + # message copy is kept + message = record.message_ids[0] - # hack to use assertEmails: filtering on from/to only is not sufficient to distinguish emails - _mails_records = [ - [mail for mail in self._mails if '%s-%s' % (record.id, record._name) in mail['message_id']] - for record in self.test_records - ] + # Translations are currently not really supported as they + # are fetched composer side. As it is a wizard people use + # and discard they are not translated. Only templates are + # translated as they are master data. + if False and exp_lang == 'es_ES': # remove False when improving support + exp_body = f'SpanishBody for {record.name}' + exp_subject = f'SpanishSubject for {record.name}' + else: + exp_body = f'TemplateBody {record.name}' + exp_subject = f'TemplateSubject {record.name}' - for record, _mails in zip(self.test_records, _mails_records): - # template is sent only to partners (email_to are transformed) - self._mails = _mails - self.assertMailMail(record.customer_id + new_partners + self.partner_admin, - 'sent', - mail_message=record.message_ids[0], - author=author, - email_values={ - 'email_from': self.partner_employee_2.email_formatted, - 'reply_to': self.partner_employee_2.email_formatted, - }, - fields_values={ - 'email_from': self.partner_employee_2.email_formatted, - 'reply_to': self.partner_employee_2.email_formatted, - }, - ) + # template is sent only to partners (email_to are transformed) + self.assertMailMail(record.customer_id + new_partners + self.partner_admin, + 'sent', + mail_message=message, + author=author, + email_values={ + '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' % record.name, 'type': 'text/plain'}, + ], + 'body_content': exp_body, + 'email_from': self.partner_employee_2.email_formatted, + 'subject': exp_subject, + }, + fields_values={ + 'email_from': self.partner_employee_2.email_formatted, + 'mail_server_id': self.mail_server_domain, + 'reply_to': formataddr(( + f'{self.env.user.company_id.name} {record.name}', + f'{self.alias_catchall}@{self.alias_domain}' + )), + 'subject': exp_subject, + }, + ) + + # Low-level checks on outgoing email for the recipient to + # check layouting and language. Note that standard layout + # is not tested against translations, only the custom one + # to ease translations checks. + email = self._find_sent_email(self.partner_employee_2.email_formatted, [record.customer_id.email_formatted]) + self.assertTrue(bool(email), 'Email not found, check recipients') + + # Currently layouting in mailing mode is not supported. + # Hence no translations. + self.assertEqual( + email['body'], + f'

TemplateBody {record.name}

' + ) + # exp_layout_content_en = 'English Layout for Ticket-like model' + # exp_layout_content_es = 'Spanish Layout para Spanish Model Description' + # exp_button_en = 'View Ticket-like model' + # exp_button_es = 'zefView Ticket-like model' + # if email_layout_xmlid: + # if exp_lang == 'es_ES': + # self.assertIn(exp_layout_content_es, email['body']) + # self.assertIn(exp_button_en, email['body']) + # else: + # self.assertIn(exp_layout_content_en, email['body']) + # # self.assertIn(exp_button_es, email['body']) + # self.assertIn(exp_button_en, email['body']) + # else: + # # check default layouting applies + # if exp_lang == 'es_ES': + # self.assertIn('html lang="es_ES"', email['body']) + # else: + # self.assertIn('html lang="en_US"', email['body']) @users('employee') @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') @@ -2020,6 +2167,36 @@ class TestComposerResultsMass(TestMailComposer): 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(self): + # test without catchall filling reply-to + 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) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False): + # remove alias so that _notify_get_reply_to will return the default value instead of alias + self.env['ir.config_parameter'].sudo().set_param("mail.catchall.domain", None) + composer.action_send_mail() + + for record in self.test_records: + # template is sent only to partners (email_to are transformed) + self.assertMailMail(record.customer_id, + 'sent', + mail_message=record.message_ids[0], + author=self.partner_employee, + email_values={ + 'email_from': self.partner_employee_2.email_formatted, + 'reply_to': self.partner_employee_2.email_formatted, + }, + fields_values={ + 'email_from': self.partner_employee_2.email_formatted, + 'reply_to': self.partner_employee_2.email_formatted, + }, + ) + @users('employee') @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') def test_mail_composer_wtpl_reply_to_force_new(self): @@ -2111,7 +2288,7 @@ class TestComposerResultsMassStatus(TestMailComposer): 'model_id': cls.env['ir.model']._get_id(cls.test_records._name), }) - def test_initial_data(self): + def test_assert_initial_data(self): """ Ensure class initial data to ease understanding """ self.assertTrue(self.template.auto_delete) diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py index 91ef5a8569c..929bd264130 100644 --- a/addons/test_mail/tests/test_message_post.py +++ b/addons/test_mail/tests/test_message_post.py @@ -5,6 +5,7 @@ import base64 from datetime import datetime, timedelta from freezegun import freeze_time +from itertools import product from markupsafe import escape from unittest.mock import patch @@ -201,12 +202,14 @@ class TestMailNotifyAPI(TestMessagePostCommon): template_values = test_record._notify_by_email_prepare_rendering_context(test_record.message_ids, {}) self.assertEqual(template_values.get('company').id, main_company.id) + @users('employee') def test_notify_recipients_internals(self): + base_record = self.test_record.with_env(self.env) pdata = self._generate_notify_recipients(self.partner_1 | self.partner_employee) msg_vals = { 'body': 'Message body', - 'model': self.test_record._name, - 'res_id': self.test_record.id, + 'model': base_record._name, + 'res_id': base_record.id, 'subject': 'Message subject', } link_vals = { @@ -216,49 +219,58 @@ class TestMailNotifyAPI(TestMessagePostCommon): 'auth_login': 'auth_login_val', } notify_msg_vals = dict(msg_vals, **link_vals) - classify_res = self.env[self.test_record._name]._notify_get_recipients_classify(pdata, 'My Custom Model Name', msg_vals=notify_msg_vals) + + # test notifying the class (void recordset) + classify_res = self.env[base_record._name]._notify_get_recipients_classify( + pdata, 'My Custom Model Name', msg_vals=notify_msg_vals + ) # find back information for each recipients partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids) emp_info = next(item for item in classify_res if item['recipients'] == self.partner_employee.ids) - # partner: no access button self.assertFalse(partner_info['has_button_access']) - # employee: access button and link self.assertTrue(emp_info['has_button_access']) for param, value in link_vals.items(): self.assertIn('%s=%s' % (param, value), emp_info['button_access']['url']) - self.assertIn('model=%s' % self.test_record._name, emp_info['button_access']['url']) - self.assertIn('res_id=%s' % self.test_record.id, emp_info['button_access']['url']) + self.assertIn('model=%s' % base_record._name, emp_info['button_access']['url']) + self.assertIn('res_id=%s' % base_record.id, emp_info['button_access']['url']) self.assertNotIn('body', emp_info['button_access']['url']) self.assertNotIn('subject', emp_info['button_access']['url']) # test when notifying on non-records (e.g. MailThread._message_notify()) - for model, res_id in ((self.test_record._name, False), - (self.test_record._name, 0), # browse(0) does not return a valid recordset - (False, self.test_record.id), + for model, res_id in ((base_record._name, False), + (base_record._name, 0), # browse(0) does not return a valid recordset + ('mail.thread', False), + ('mail.thread', base_record.id)): + with self.subTest(model=model, res_id=res_id): + notify_msg_vals.update({ + 'model': model, + 'res_id': res_id, + }) + classify_res = self.env[model].browse(res_id)._notify_get_recipients_classify( + pdata, 'Test', msg_vals=notify_msg_vals) + # find back information for partner + partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids) + emp_info = next(item for item in classify_res if item['recipients'] == self.partner_employee.ids) + # check there is no access button + self.assertFalse(partner_info['has_button_access']) + self.assertFalse(emp_info['has_button_access']) + + # test when notifying based a valid record, but asking for a falsy record in msg_vals + for model, res_id in ((base_record._name, False), + (base_record._name, 0), # browse(0) does not return a valid recordset + (False, base_record.id), (False, False), ('mail.thread', False), - ('mail.thread', self.test_record.id)): - msg_vals.update({ - 'model': model, - 'res_id': res_id, - }) - # note that msg_vals wins over record on which method is called - notify_msg_vals = dict(msg_vals, **link_vals) - classify_res = self.test_record._notify_get_recipients_classify( - pdata, 'Test', msg_vals=notify_msg_vals) - # find back information for partner - partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids) - emp_info = next(item for item in classify_res if item['recipients'] == self.partner_employee.ids) - # check there is no access button - self.assertFalse(partner_info['has_button_access']) - self.assertFalse(emp_info['has_button_access']) - - # test on falsy records (False model cannot be browsed, skipped) - if model: - record_falsy = self.env[model].browse(res_id) - classify_res = record_falsy._notify_get_recipients_classify( + ('mail.thread', base_record.id)): + with self.subTest(model=model, res_id=res_id): + # note that msg_vals wins over record on which method is called + notify_msg_vals.update({ + 'model': model, + 'res_id': res_id, + }) + classify_res = base_record._notify_get_recipients_classify( pdata, 'Test', msg_vals=notify_msg_vals) # find back information for partner partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids) @@ -436,7 +448,7 @@ class TestMessageNotify(TestMessagePostCommon): ) @users('employee') - @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests') + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') def test_notify_parameters(self): """ Test usage of parameters in notify, both for unwanted side effects and magic parameters. """ @@ -591,7 +603,8 @@ class TestMessageLog(TestMessagePostCommon): @tagged('mail_post') class TestMessagePost(TestMessagePostCommon, CronMixinCase): - def test_initial_values(self): + def test_assert_initial_values(self): + """ Be sure of what we are testing """ self.assertFalse(self.test_record.message_ids) self.assertFalse(self.test_record.message_follower_ids) self.assertFalse(self.test_record.message_partner_ids) @@ -662,7 +675,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase): partner_ids=self.partner_portal.ids, ) - @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests') @users('employee') def test_message_post_author(self): """ Test author recognition """ @@ -765,6 +778,7 @@ class TestMessagePost(TestMessagePostCommon, CronMixinCase): ) @users('employee') + @mute_logger('odoo.models.unlink') def test_message_post_inactive_follower(self): """ Test posting with inactive followers does not notify them (e.g. odoobot) """ test_record = self.env['mail.test.simple'].browse(self.test_record.ids) @@ -1480,46 +1494,103 @@ class TestMessagePostLang(TestMailCommon, TestRecipients): cls.partner_2.write({'lang': 'es_ES'}) + def test_assert_initial_values(self): + """ Be sure of what we are testing """ + self.assertEqual(self.partner_1.lang, 'en_US') + self.assertEqual(self.partner_2.lang, 'es_ES') + + self.assertEqual(self.test_records[0].lang, 'es_ES') + self.assertEqual(self.test_records[0].customer_id.lang, False) + self.assertEqual(self.test_records[1].lang, False) + self.assertEqual(self.test_records[1].customer_id.lang, 'es_ES') + + self.assertFalse(self.test_records[0].message_follower_ids) + self.assertFalse(self.test_records[1].message_follower_ids) + + self.assertEqual(self.user_employee.lang, 'en_US') + @users('employee') @mute_logger('odoo.addons.mail.models.mail_mail') def test_composer_lang_template_comment(self): + """ When posting in comment mode, content is rendered using the lang + field of template. Notification layout lang is partly coming from + template, partly from environment (to be improved). """ test_record = self.test_records[0].with_user(self.env.user) test_template = self.test_template.with_user(self.env.user) - with self.mock_mail_gateway(): - test_record.message_post_with_source( - test_template, - email_layout_xmlid='mail.test_layout', - message_type='comment', - subtype_id=self.env.ref('mail.mt_comment').id, - ) + for partner in self.env['res.partner'] + self.partner_1 + self.partner_2: + with self.subTest(partner=partner): + test_record.write({ + 'customer_id': partner.id, + }) + with self.mock_mail_gateway(): + test_record.message_post_with_source( + test_template, + email_layout_xmlid='mail.test_layout', + message_type='comment', + subtype_id=self.env.ref('mail.mt_comment').id, + ) - record0_customer = self.env['res.partner'].search([('email_normalized', '=', 'test.record.1@test.customer.com')], limit=1) - self.assertTrue(record0_customer, 'Template usage should have created a contact based on record email') + # expected languages: content depend on template (lang field) aka + # customer.lang or record.lang (see template); notif lang is + # partner lang or default DB lang + exp_content_lang = partner.lang if partner.lang else 'es_ES' + exp_notif_lang = partner.lang if partner.lang else 'en_US' - customer_email = self._find_sent_mail_wemail(record0_customer.email_formatted) - self.assertTrue(customer_email) - body = customer_email['body'] - # check content - self.assertIn(f'SpanishBody for {test_record.name}', body, - 'Body based on template should be translated') - # check subject - self.assertEqual(f'SpanishSubject for {test_record.name}', customer_email['subject'], - 'Subject based on template should be translated') - # check notification layout content - self.assertIn('Spanish Layout para', body, 'Layout content should be translated') - self.assertNotIn('English Layout for', body) - self.assertIn('Spanish Layout para Spanish Model Description', body, 'Model name should be translated') - # check notification layout strings - self.assertIn('View Lang Chatter Model', body, - 'Fixme: "View document" should be translated') - # self.assertIn('SpanishView Spanish Model Description', body, - # '"View document" should be translated') - # self.assertNotIn(f'View {test_record._description}', body, - # '"View document" should be translated') - # self.assertIn('SpanishButtonTitle', body, - # 'Groups-based action names should be translated') - self.assertIn('NotificationButtonTitle', body, 'Fixme: Groups-based action names should be translated') + if partner: + customer = partner + else: + customer = self.env['res.partner'].search([('email_normalized', '=', 'test.record.1@test.customer.com')], limit=1) + self.assertTrue(customer, 'Template usage should have created a contact based on record email') + self.assertEqual(customer.lang, exp_notif_lang) + + customer_email = self._find_sent_mail_wemail(customer.email_formatted) + self.assertTrue(customer_email) + body = customer_email['body'] + # check content: depends on object.lang / object.customer_id.lang + if exp_content_lang == 'en_US': + self.assertIn(f'EnglishBody for {test_record.name}', body, + 'Body based on template should be translated') + else: + self.assertIn(f'SpanishBody for {test_record.name}', body, + 'Body based on template should be translated') + # check subject + if exp_content_lang == 'en_US': + self.assertEqual(f'EnglishSubject for {test_record.name}', customer_email['subject'], + 'Subject based on template should be translated') + else: + self.assertEqual(f'SpanishSubject for {test_record.name}', customer_email['subject'], + 'Subject based on template should be translated') + # check notification layout content: currently partly translated + # based only on template definition + if exp_content_lang == 'en_US': + self.assertNotIn('Spanish Layout para', body, 'Layout translation failed') + self.assertIn('English Layout for Lang Chatter Model', body, + 'Layout / model translation failed') + self.assertNotIn('Spanish Model Description', body, 'Model translation failed') + # check notification layout strings + self.assertNotIn('SpanishView Spanish Model Description', body, + '"View document" translation failed') + self.assertIn(f'View {test_record._description}', body, + '"View document" translation failed') + self.assertNotIn('SpanishButtonTitle', body, + 'Groups-based action names translation failed') + self.assertIn('NotificationButtonTitle', body, + 'Groups-based action names translation failed') + else: + self.assertIn('Spanish Layout para', body, 'Layout content should be translated') + self.assertNotIn('English Layout for', body) + self.assertIn('Spanish Layout para Spanish Model Description', body, 'Model name should be translated') + # check notification layout strings + self.assertIn('View Lang Chatter Model', body, + 'Fixme: "View document" should be translated') + # self.assertIn('SpanishView Spanish Model Description', body, + # '"View document" should be translated') + # self.assertNotIn(f'View {test_record._description}', body, + # '"View document" should be translated') + # self.assertIn('SpanishButtonTitle', body, + # 'Groups-based action names should be translated') + self.assertIn('NotificationButtonTitle', body, 'Fixme: Groups-based action names should be translated') @users('employee') @mute_logger('odoo.addons.mail.models.mail_mail') @@ -1589,6 +1660,8 @@ class TestMessagePostLang(TestMailCommon, TestRecipients): @users('employee') @mute_logger('odoo.addons.mail.models.mail_mail') def test_layout_email_lang_template(self): + """ Test language support when posting in batch using a template. + Content and layout are is translated based on template definition. """ test_records = self.test_records.with_user(self.env.user) test_template = self.test_template.with_user(self.env.user) @@ -1606,6 +1679,10 @@ class TestMessagePostLang(TestMailCommon, TestRecipients): for record, customer in zip(test_records, record0_customer + self.partner_2): customer_email = self._find_sent_mail_wemail(customer.email_formatted) self.assertTrue(customer_email) + + # body and layouting are translated partly based on template. Bits + # of layout are not translated due to lang not being correctly + # propagate everywhere we need it body = customer_email['body'] # check content self.assertIn(f'SpanishBody for {record.name}', body, @@ -1627,3 +1704,90 @@ class TestMessagePostLang(TestMailCommon, TestRecipients): # 'Groups-based action names should be translated') self.assertIn('NotificationButtonTitle', body, 'Fixme: groups-based action names should be translated') + + @users('employee') + @mute_logger('odoo.addons.mail.models.mail_mail') + def test_post_multi_lang_recipients(self): + """ Test posting on a document in a multilang environment. Currently + current user's lang determines completely language used for notification + layout notably, when no template is involved. + + Lang layout for this test (to better check various configuration and + check which lang wins the final output, if any) + + * current users: various between en and es; + * partner1: es + * partner2: en + """ + test_records = self.test_records.with_env(self.env) + test_records.message_subscribe(partner_ids=(self.partner_1 + self.partner_2).ids) + + for employee_lang, email_layout_xmlid in product( + ('en_US', 'es_ES'), + (False, 'mail.test_layout'), + ): + with self.subTest(employee_lang=employee_lang, email_layout_xmlid=email_layout_xmlid): + self.user_employee.write({ + 'lang': employee_lang, + }) + for record in test_records: + with self.mock_mail_gateway(mail_unlink_sent=False), \ + self.mock_mail_app(): + record.message_post( + body='

Hi there

', + email_layout_xmlid=email_layout_xmlid, + message_type='comment', + subject='TeDeum', + subtype_xmlid='mail.mt_comment', + ) + message = record.message_ids[0] + self.assertEqual( + message.notified_partner_ids, self.partner_1 + self.partner_2 + ) + + # check created mail.mail and outgoing emails. One email + # is generated for 'partner_1' and 'partner_2' (same email + # as same configuration (aka all customers)). + _mail = self.assertMailMail( + self.partner_1 + self.partner_2, 'sent', + mail_message=message, + author=self.partner_employee, + email_values={ + 'body_content': '

Hi there

', + 'email_from': self.partner_employee.email_formatted, + 'subject': 'TeDeum', + }, + ) + + # Low-level checks on outgoing email for the recipient to + # check layouting and language. Note that standard layout + # is not tested against translations, only the custom one + # to ease translations checks. + for partner in self.partner_1 + self.partner_2: + email = self._find_sent_email( + self.partner_employee.email_formatted, + [partner.email_formatted] + ) + self.assertTrue(bool(email), 'Email not found, check recipients') + + exp_layout_content_en = 'English Layout for Lang Chatter Model' + exp_layout_content_es = 'Spanish Layout para Spanish Model Description' + exp_button_en = 'View Lang Chatter Model' + exp_button_es = 'SpanishView Spanish Model Description' + exp_action_en = 'NotificationButtonTitle' + exp_action_es = 'SpanishButtonTitle' + if email_layout_xmlid: + if employee_lang == 'es_ES': + self.assertIn(exp_layout_content_es, email['body']) + self.assertIn(exp_button_es, email['body']) + self.assertIn(exp_action_es, email['body']) + else: + self.assertIn(exp_layout_content_en, email['body']) + self.assertIn(exp_button_en, email['body']) + self.assertIn(exp_action_en, email['body']) + else: + # check default layouting applies + if employee_lang == 'es_ES': + self.assertIn('html lang="es_ES"', email['body']) + else: + self.assertIn('html lang="en_US"', email['body'])