diff --git a/addons/event/tests/test_event_mail_schedule.py b/addons/event/tests/test_event_mail_schedule.py index 7b9bb735c42..34eacf1d701 100644 --- a/addons/event/tests/test_event_mail_schedule.py +++ b/addons/event/tests/test_event_mail_schedule.py @@ -120,7 +120,8 @@ class TestMailSchedule(TestEventCommon, MockEmail): self.assertEqual(len(self._new_mails), 2, 'event: should have 2 scheduled emails (1 / registration)') self.assertMailMailWEmails( [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))], - 'outgoing', '', + 'outgoing', + content=None, fields_values={'subject': 'Your registration at %s' % test_event.name, 'email_from': self.user_eventmanager.company_id.email_formatted, }) @@ -157,7 +158,8 @@ class TestMailSchedule(TestEventCommon, MockEmail): self.assertEqual(len(self._new_mails), 2, 'event: should have 2 scheduled emails (1 / registration)') self.assertMailMailWEmails( [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))], - 'outgoing', '', + 'outgoing', + content=None, fields_values={'subject': 'Your registration at %s' % test_event.name, 'email_from': self.user_eventmanager.company_id.email_formatted, }) @@ -191,7 +193,8 @@ class TestMailSchedule(TestEventCommon, MockEmail): self.assertEqual(len(self._new_mails), 2, 'event: should have scheduled 2 mails (1 / registration)') self.assertMailMailWEmails( [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))], - 'outgoing', '', + 'outgoing', + content=None, fields_values={'subject': '%s: tomorrow' % test_event.name, 'email_from': self.user_eventmanager.company_id.email_formatted, }) @@ -265,7 +268,8 @@ class TestMailSchedule(TestEventCommon, MockEmail): self.assertEqual(len(self._new_mails), 3, 'event: should have scheduled 3 mails, one for each registration') self.assertMailMailWEmails( [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email)), formataddr((reg3.name, reg3.email))], - 'outgoing', '', + 'outgoing', + content=None, fields_values={'subject': '%s: today' % test_event.name, 'email_from': self.user_eventmanager.company_id.email_formatted, }) diff --git a/addons/mail/models/mail_render_mixin.py b/addons/mail/models/mail_render_mixin.py index 6211cde0046..68728726b11 100644 --- a/addons/mail/models/mail_render_mixin.py +++ b/addons/mail/models/mail_render_mixin.py @@ -258,6 +258,19 @@ class MailRenderMixin(models.AbstractModel): @api.model def _render_template_qweb(self, template_src, model, res_ids, add_context=None): + """ Render a QWeb template. + + :param str template_src: source QWeb template. It should be a string + XmlID allowing to fetch an ir.ui.view; + :param str model: see ``MailRenderMixin._render_field)``; + :param list res_ids: see ``MailRenderMixin._render_field)``; + + :param dict add_context: additional context to give to renderer. It + allows to add values to base rendering context generated by + ``MailRenderMixin._render_qweb_eval_context()``; + + :return dict: {res_id: string of rendered template based on record} + """ view = self.env.ref(template_src, raise_if_not_found=False) or self.env['ir.ui.view'] results = dict.fromkeys(res_ids, u"") if not view: @@ -370,15 +383,14 @@ class MailRenderMixin(models.AbstractModel): @api.model def _render_template(self, template_src, model, res_ids, engine='jinja', add_context=None, post_process=False): """ Render the given string on records designed by model / res_ids using - the given rendering engine. Currently only jinja is supported. + the given rendering engine. Currently only jinja or qweb are supported. :param str template_src: template text to render (jinja) or xml id of view (qweb) this could be cleaned but hey, we are in a rush :param str model: model name of records on which we want to perform rendering :param list res_ids: list of ids of records (all belonging to same model) :param string engine: jinja - :param post_process: perform rendered str / html post processing (see - ``_render_template_postprocess``) + :param post_process: see ``MailRenderMixin._render_field``; :return dict: {res_id: string of rendered template based on record} """ @@ -437,16 +449,25 @@ class MailRenderMixin(models.AbstractModel): for lang, lang_res_ids in lang_to_res_ids.items() ) - def _render_field(self, field, res_ids, compute_lang=False, set_lang=False, post_process=False): - """ Given some record ids, render a given field of template rendered on - all records. + def _render_field(self, field, res_ids, + compute_lang=False, set_lang=False, + post_process=False): + """ Given some record ids, render a template located on field on all + records. ``field`` should be a field of self (i.e. ``body_html`` on + ``mail.template``). res_ids are record IDs linked to ``model`` field + on self. :param list res_ids: list of ids of records (all belonging to same model - defined by self.model) - :param compute_lang: compute rendering language based on template.lang - :param set_lang: force language - :param post_process: perform rendered str / html post processing (see - ``_render_template_postprocess``) + defined by ``self.model``) + + :param boolean compute_lang: compute language to render on translated + version of the template instead of default (probably english) one. + Language will be computed based on ``self.lang``; + :param string set_lang: force language for rendering. It should be a + valid lang code matching an activate res.lang. Checked only if + ``compute_lang`` is False; + :param boolean post_process: perform a post processing on rendered result + (notably html links management). See``_render_template_postprocess``); :return dict: {res_id: string of rendered template based on record} """ diff --git a/addons/mail/tests/__init__.py b/addons/mail/tests/__init__.py index f275a83b1b0..4a99ea63e4a 100644 --- a/addons/mail/tests/__init__.py +++ b/addons/mail/tests/__init__.py @@ -4,5 +4,6 @@ from . import test_mail_channel from . import test_mail_channel_partner from . import test_mail_full_composer +from . import test_mail_render from . import test_res_partner from . import test_update_notification diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index d0ff1259e20..bf3f3707008 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -8,6 +8,7 @@ import time from collections import defaultdict from contextlib import contextmanager from functools import partial +from lxml import html from unittest.mock import patch from smtplib import SMTPServerDisconnected @@ -41,8 +42,8 @@ class MockEmail(common.BaseCase): build_email_origin = IrMailServer.build_email mail_create_origin = MailMail.create mail_unlink_origin = MailMail.unlink - self._init_mail_mock() self.mail_unlink_sent = mail_unlink_sent + self._init_mail_mock() def _ir_mail_server_connect(model, *args, **kwargs): if sim_error and sim_error == 'connect_smtp_notfound': @@ -151,8 +152,6 @@ class MockEmail(common.BaseCase): return email.message_from_string(pycompat.to_text(text), policy=email.policy.SMTP) def assertHtmlEqual(self, value, expected, message=None): - from lxml import html - tree = html.fragment_fromstring(value, parser=html.HTMLParser(encoding='utf-8'), create_parent='body') # mass mailing: add base tag we have to remove @@ -170,19 +169,38 @@ class MockEmail(common.BaseCase): self.assertEqual(tree, expected_node) # ------------------------------------------------------------ - # GATEWAY ASSERTS + # GATEWAY GETTERS # ------------------------------------------------------------ def _find_sent_mail_wemail(self, email_to): - for email in self._mails: - if set(email['email_to']) == set([email_to]): + """ Find a sent email with a given list of recipients. Email should match + exactly the recipients. + + :return email: a dictionary mapping values given to ``build_email``; + """ + for sent_email in self._mails: + if set(sent_email['email_to']) == set([email_to]): break else: raise AssertionError('sent mail not found for email_to %s' % (email_to)) - return email - + return sent_email def _find_mail_mail_wpartners(self, recipients, status, mail_message=None, author=None): + """ Find a mail.mail record based on various parameters, notably a list + of recipients (partners). + + :param recipients: a ``res.partner`` recordset Check all of them are in mail + recipients to find the right mail.mail record; + :param status: state of mail.mail. If not void use it to filter mail.mail + record; + :param mail_message: optional check/filter on mail_message_id field aka + a ``mail.message`` record; + :param author: optional check/filter on author_id field aka a ``res.partner`` + record; + + :return mail: a ``mail.mail`` record generated during the mock and matching + given parameters and filters; + """ for mail in self._new_mails: if author is not None and mail.author_id != author: continue @@ -193,10 +211,25 @@ class MockEmail(common.BaseCase): if all(p in mail.recipient_ids for p in recipients): break else: - raise AssertionError('mail.mail not found for message %s / status %s / recipients %s' % (mail_message, status, recipients.ids)) + raise AssertionError('mail.mail not found for message %s / status %s / recipients %s / author %s' % (mail_message, status, recipients.ids, author)) return mail def _find_mail_mail_wemail(self, email_to, status, mail_message=None, author=None): + """ Find a mail.mail record based on various parameters, notably a list + of email to (string emails). + + :param email_to: either matching mail.email_to value, either a mail sent + to a single recipient whose email is email_to; + :param status: state of mail.mail. If not void use it to filter mail.mail + record; + :param mail_message: optional check/filter on mail_message_id field aka + a ``mail.message`` record; + :param author: optional check/filter on author_id field aka a ``res.partner`` + record; + + :return mail: a ``mail.mail`` record generated during the mock and matching + given parameters and filters; + """ for mail in self._new_mails: if author is not None and mail.author_id != author: continue @@ -204,13 +237,17 @@ class MockEmail(common.BaseCase): continue if status and mail.state != status: continue - if (mail.email_to == email_to and not mail.recipient_ids) or (not mail.email_to and mail.recipient_ids.email) == email_to: + if (mail.email_to == email_to and not mail.recipient_ids) or (not mail.email_to and mail.recipient_ids.email == email_to): break else: raise AssertionError('mail.mail not found for email_to %s / status %s in %s' % (email_to, status, repr([m.email_to for m in self._new_mails]))) return mail def _find_mail_mail_wrecord(self, record): + """ Find a mail.mail record based on model / res_id of a record. + + :return mail: a ``mail.mail`` record generated during the mock; + """ for mail in self._new_mails: if mail.model == record._name and mail.res_id == record.id: break @@ -218,39 +255,81 @@ class MockEmail(common.BaseCase): raise AssertionError('mail.mail not found for record %s in %s' % (record, repr([m.email_to for m in self._new_mails]))) return mail - def assertMailFailed(self, author, recipients, mail_message): - mail = self._find_mail(author, recipients, mail_message) - self.assertEqual(mail.state, 'exception') + # ------------------------------------------------------------ + # GATEWAY ASSERTS + # ------------------------------------------------------------ - def assertMailMail(self, recipients, status, check_mail_mail=True, mail_message=None, author=None, email_values=None, fields_values=None): - if check_mail_mail: - mail = self._find_mail_mail_wpartners(recipients, status, mail_message=mail_message, author=author) - self.assertTrue(bool(mail)) - for fname, fvalue in (fields_values or {}).items(): - self.assertEqual(mail[fname], fvalue) + def assertMailMail(self, recipients, status, + check_mail_mail=True, mail_message=None, author=None, + content=None, fields_values=None, email_values=None): + """ Assert mail.mail records are created and maybe sent as emails. Allow + asserting their content. Records to check are the one generated when + using mock (mail.mail and outgoing emails). This method takes partners + as source of record fetch and assert. + + :param recipients: a ``res.partner`` recordset. See ``_find_mail_mail_wpartners``; + :param status: mail.mail state used to filter mails. If ``sent`` this method + also check that emails have been sent trough gateway; + :param mail_message: see ``_find_mail_mail_wpartners``; + :param author: see ``_find_mail_mail_wpartners``;; + :param content: if given, check it is contained within mail html body; + :param fields_values: if given, should be a dictionary of field names / + values allowing to check ``mail.mail`` additional values (subject, + reply_to, ...); + :param email_values: if given, should be a dictionary of keys / values + allowing to check sent email additional values (if any). + See ``assertSentEmail``; + + :param check_mail_mail: deprecated, use ``assertSentEmail`` if False + """ + found_mail = self._find_mail_mail_wpartners(recipients, status, mail_message=mail_message, author=author) + self.assertTrue(bool(found_mail)) + if content: + self.assertIn(content, found_mail.body_html) + for fname, fvalue in (fields_values or {}).items(): + self.assertEqual( + found_mail[fname], fvalue, + 'Mail: expected %s for %s, got %s' % (fvalue, fname, found_mail[fname])) if status == 'sent': for recipient in recipients: - self.assertSentEmail(author, [recipient], **email_values) + self.assertSentEmail(email_values['email_from'] if email_values and email_values.get('email_from') else author, [recipient], **(email_values or {})) - def assertMailMailWEmails(self, emails, status, content, fields_values=None): - """ Will check in self._new_mails to find a sent mail.mail. To use with - mail gateway mock. + def assertMailMailWEmails(self, emails, status, + mail_message=None, author=None, + content=None, fields_values=None, email_values=None): + """ Assert mail.mail records are created and maybe sent as emails. Allow + asserting their content. Records to check are the one generated when + using mock (mail.mail and outgoing emails). This method takes emails + as source of record fetch and assert. - :param emails: list of emails; - :param status: status of mail.mail; - :param content: content to check for each email; - :param fields_values: specific value to check on the mail.mail record; + :param emails: a list of emails. See ``_find_mail_mail_wemail``; + :param status: mail.mail state used to filter mails. If ``sent`` this method + also check that emails have been sent trough gateway; + :param mail_message: see ``_find_mail_mail_wemail``; + :param author: see ``_find_mail_mail_wemail``;; + :param content: if given, check it is contained within mail html body; + :param fields_values: if given, should be a dictionary of field names / + values allowing to check ``mail.mail`` additional values (subject, + reply_to, ...); + :param email_values: if given, should be a dictionary of keys / values + allowing to check sent email additional values (if any). + See ``assertSentEmail``; + + :param check_mail_mail: deprecated, use ``assertSentEmail`` if False """ for email_to in emails: - sent_mail = self._find_mail_mail_wemail(email_to, status) + found_mail = self._find_mail_mail_wemail(email_to, status, mail_message=mail_message, author=author) if content: - self.assertIn(content, sent_mail.body_html) + self.assertIn(content, found_mail.body_html) for fname, fvalue in (fields_values or {}).items(): self.assertEqual( - sent_mail[fname], fvalue, - 'Mail: expected %s for %s, got %s' % (fvalue, fname, sent_mail[fname])) + found_mail[fname], fvalue, + 'Mail: expected %s for %s, got %s' % (fvalue, fname, found_mail[fname])) + if status == 'sent': + for email_to in emails: + self.assertSentEmail(email_values['email_from'] if email_values and email_values.get('email_from') else author, [email_to], **(email_values or {})) - def assertNoMail(self, author, recipients, mail_message=None): + def assertNoMail(self, recipients, mail_message=None, author=None): try: self._find_mail_mail_wpartners(recipients, False, mail_message=mail_message, author=author) except AssertionError: @@ -302,6 +381,15 @@ class MockEmail(common.BaseCase): for val in ['reply_to', 'subject', 'references', 'attachments']: if val in expected: self.assertEqual(expected[val], sent_mail[val], 'Value for %s: expected %s, received %s' % (val, expected[val], sent_mail[val])) + if 'attachments_info' in values: + attachments = sent_mail['attachments'] + for attachment_info in values['attachments_info']: + attachment = next(attach for attach in attachments if attach[0] == attachment_info['name']) + if attachment_info.get('raw'): + self.assertEqual(attachment[1], attachment_info['raw']) + if attachment_info.get('type'): + self.assertEqual(attachment[2], attachment_info['type']) + self.assertEqual(len(values['attachments_info']), len(attachments)) for val in ['body']: if val in expected: self.assertHtmlEqual(expected[val], sent_mail[val], 'Value for %s: expected %s, received %s' % (val, expected[val], sent_mail[val])) @@ -591,18 +679,27 @@ class MailCase(MockEmail): for recipients in email_groups.values(): partners = self.env['res.partner'].sudo().concat(*recipients) if all(p in mail_groups['failure'] for p in partners): - self.assertMailMail(partners, 'exception', author=message.author_id, mail_message=message) + if not self.mail_unlink_sent: + self.assertMailMail(partners, 'exception', + author=message.author_id, + mail_message=message) + else: + for recipient in partners: + self.assertSentEmail(message.author_id, [recipient]) else: - check_mail_mail = not self.mail_unlink_sent - self.assertMailMail( - partners, 'sent', - author=message.author_id if message.author_id else message.email_from, - mail_message=message, - check_mail_mail=check_mail_mail, - email_values={'body_content': mbody} - ) + if not self.mail_unlink_sent: + self.assertMailMail( + partners, 'sent', + author=message.author_id if message.author_id else message.email_from, + mail_message=message, + email_values={'body_content': mbody} + ) + else: + for recipient in partners: + self.assertSentEmail(message.author_id if message.author_id else message.email_from, [recipient], + email_values={'body_content': mbody}) if not any(p for recipients in email_groups.values() for p in recipients): - self.assertNoMail(message.author_id, partners, message) + self.assertNoMail(partners, mail_message=message, author=message.author_id) return done_msgs, done_notifs @@ -655,6 +752,29 @@ class MailCase(MockEmail): return bus_notifs + def assertNotified(self, message, recipients_info, is_complete=False): + """ Lightweight check for notifications (mail.notification). + + :param recipients_info: list notified recipients: [ + {'partner': res.partner record (may be empty), + 'type': notification_type to check, + 'is_read': is_read to check, + }, {...}] + """ + notifications = self._new_notifs.filtered(lambda notif: notif in message.notification_ids) + if is_complete: + self.assertEqual(len(notifications), len(recipients_info)) + for rinfo in recipients_info: + recipient_notif = next( + (notif + for notif in notifications + if notif.res_partner_id == rinfo['partner'] + ), False + ) + self.assertTrue(recipient_notif) + self.assertEqual(recipient_notif.is_read, rinfo['is_read']) + self.assertEqual(recipient_notif.notification_type, rinfo['type']) + def assertTracking(self, message, data): tracking_values = message.sudo().tracking_value_ids for field_name, value_type, old_value, new_value in data: diff --git a/addons/mail/tests/test_mail_channel.py b/addons/mail/tests/test_mail_channel.py index 99f4f3ac9e2..f1157f077a5 100644 --- a/addons/mail/tests/test_mail_channel.py +++ b/addons/mail/tests/test_mail_channel.py @@ -508,8 +508,11 @@ class TestChannelModeration(MailCommon): (msg_c1_admin1 | msg_c1_emplo2).with_user(self.user_employee)._moderate('reject', title='RejectTitle', comment='RejectComment') self.assertEqual(len(self._new_mails), 2) - self.assertMailMailWEmails([self.partner_admin.email_formatted, self.partner_employee_2.email_formatted], 'outgoing', 'RejectComment', - fields_values={'subject': 'RejectTitle', 'author_id': self.partner_employee}) + self.assertMailMailWEmails( + [self.partner_admin.email_formatted, self.partner_employee_2.email_formatted], 'outgoing', + content='RejectComment', + fields_values={'subject': 'RejectTitle', 'author_id': self.partner_employee} + ) self.assertTrue(all('RejectComment' in body for body in self._new_mails.mapped('body_html'))) self.assertTrue(all('Body' in body for body in self._new_mails.mapped('body_html'))) @@ -674,7 +677,7 @@ class TestChannelModeration(MailCommon): }) # admin being banned, should not received any email - self.assertNoMail(self.partner_employee, self.partner_admin) + self.assertNoMail(self.partner_employee, author=self.partner_admin) # only moderators can send guidelines channel.write({ diff --git a/addons/mail/tests/test_mail_render.py b/addons/mail/tests/test_mail_render.py new file mode 100644 index 00000000000..2f14cbca97e --- /dev/null +++ b/addons/mail/tests/test_mail_render.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.mail.tests import common +from odoo.tests import tagged, users + + +@tagged('mail_render') +class TestMailRender(common.MailCommon): + + @classmethod + def setUpClass(cls): + super(TestMailRender, cls).setUpClass() + + # activate multi language support + cls.env['res.lang']._activate_lang('fr_FR') + cls.user_admin.write({'lang': 'en_US'}) + + # test records + cls.render_object = cls.env['res.partner'].create({ + 'name': 'TestRecord', + 'lang': 'en_US', + }) + cls.render_object_fr = cls.env['res.partner'].create({ + 'name': 'Element de Test', + 'lang': 'fr_FR', + }) + + # some jinja templates + cls.base_jinja_bits = [ + '

Hello

', + '

Hello ${object.name}

', + """

+% set english = object.lang == 'en_US' +% if english + English Speaker +% else + Other Speaker +% endif +

""" + ] + cls.base_jinja_bits_fr = [ + '

Bonjour

', + '

Bonjour ${object.name}

', + """

+% set english = object.lang == 'en_US' +% if english + Narrateur Anglais +% else + Autre Narrateur +% endif +

""" + ] + + # some qweb templates and their xml ids + cls.base_qweb_templates = cls.env['ir.ui.view'].create([ + {'name': 'TestRender', 'type': 'qweb', + 'arch': '

Hello

', + }, + {'name': 'TestRender2', 'type': 'qweb', + 'arch': '

Hello

', + }, + {'name': 'TestRender3', 'type': 'qweb', + 'arch': """

+ English Speaker + Other Speaker +

""", + }, + ]) + cls.base_qweb_templates_data = cls.env['ir.model.data'].create([ + {'name': template.name, 'module': 'mail', + 'model': template._name, 'res_id': template.id, + } for template in cls.base_qweb_templates + ]) + cls.base_qweb_templates_xmlids = [ + model_data.complete_name + for model_data in cls.base_qweb_templates_data + ] + + # render result + cls.base_rendered = [ + '

Hello

', + '

Hello %s

' % cls.render_object.name, + """

+ English Speaker +

""" + ] + cls.base_rendered_fr = [ + '

Bonjour

', + '

Bonjour %s

' % cls.render_object_fr.name, + """

+ Autre Narrateur +

""" + ] + + # link to mail template + cls.test_template_jinja = cls.env['mail.template'].create({ + 'name': 'Test Template', + 'subject': cls.base_jinja_bits[0], + 'body_html': cls.base_jinja_bits[1], + 'model_id': cls.env['ir.model']._get('res.partner').id, + 'lang': '${object.lang}' + }) + + # some translations + cls.env['ir.translation'].create({ + 'type': 'model', + 'name': 'mail.template,subject', + 'lang': 'fr_FR', + 'res_id': cls.test_template_jinja.id, + 'src': cls.test_template_jinja.subject, + 'value': cls.base_jinja_bits_fr[0], + }) + cls.env['ir.translation'].create({ + 'type': 'model', + 'name': 'mail.template,body_html', + 'lang': 'fr_FR', + 'res_id': cls.test_template_jinja.id, + 'src': cls.test_template_jinja.body_html, + 'value': cls.base_jinja_bits_fr[1], + }) + cls.env['ir.model.data'].create({ + 'name': 'test_template_xmlid', + 'module': 'mail', + 'model': cls.test_template_jinja._name, + 'res_id': cls.test_template_jinja.id, + }) + + @users('employee') + def test_render_jinja(self): + source = """

+% set line_statement_variable = 3 +We have ${line_statement_variable} cookies in stock +We have <% set block_variable = 4 %>${block_variable} cookies in stock +

""" + partner = self.env['res.partner'].browse(self.render_object.ids) + result = self.env['mail.render.mixin']._render_template( + source, + partner._name, + partner.ids, + engine='jinja', + )[partner.id] + self.assertEqual(result, """

+We have 3 cookies in stock +We have 4 cookies in stock +

""") + + @users('employee') + def test_render_mail_template_jinja(self): + template = self.env['mail.template'].browse(self.test_template_jinja.ids) + partner = self.env['res.partner'].browse(self.render_object.ids) + for fname, expected in zip(['subject', 'body_html'], self.base_rendered): + rendered = template._render_field( + fname, + partner.ids, + compute_lang=True + )[partner.id] + self.assertEqual(rendered, expected) + + partner = self.env['res.partner'].browse(self.render_object_fr.ids) + for fname, expected in zip(['subject', 'body_html'], self.base_rendered_fr): + rendered = template._render_field( + fname, + partner.ids, + compute_lang=True + )[partner.id] + self.assertEqual(rendered, expected) + + @users('employee') + def test_render_template_jinja(self): + partner = self.env['res.partner'].browse(self.render_object.ids) + for source, expected in zip(self.base_jinja_bits, self.base_rendered): + rendered = self.env['mail.render.mixin']._render_template( + source, + partner._name, + partner.ids, + engine='jinja', + )[partner.id] + self.assertEqual(rendered, expected) + + @users('employee') + def test_render_template_qweb(self): + partner = self.env['res.partner'].browse(self.render_object.ids) + for source, expected in zip(self.base_qweb_templates_xmlids, self.base_rendered): + rendered = self.env['mail.render.mixin']._render_template( + source, + partner._name, + partner.ids, + engine='qweb', + )[partner.id].decode() + self.assertEqual(rendered, expected) diff --git a/addons/mass_mailing/tests/common.py b/addons/mass_mailing/tests/common.py index 8c3925d7527..512e1c73a2f 100644 --- a/addons/mass_mailing/tests/common.py +++ b/addons/mass_mailing/tests/common.py @@ -7,7 +7,8 @@ from odoo.addons.mail.tests.common import MailCase, MailCommon, mail_new_test_us class MassMailCase(MailCase, MockLinkTracker): - def assertMailTraces(self, recipients_info, mailing, records, check_mail=True): + def assertMailTraces(self, recipients_info, mailing, records, + check_mail=True, author=None): """ Check content of traces. :param recipients_info: list[{ @@ -19,6 +20,15 @@ class MassMailCase(MailCase, MockLinkTracker): 'failure_type': optional: UPDATE ME }, { ... }] """ + # map trace state to email state + state_mapping = { + 'sent': 'sent', + 'replied': 'sent', # replied imply something has been sent + 'ignored': 'cancel', + 'exception': 'exception', + 'canceled': 'canceled', + } + traces = self.env['mailing.trace'].search([ ('mass_mailing_id', 'in', mailing.ids), ('res_id', 'in', records.ids) @@ -47,22 +57,17 @@ class MassMailCase(MailCase, MockLinkTracker): ) if check_mail: + if author is None: + author = self.env.user.partner_id + fields_values = {'mailing_id': mailing} if 'failure_type' in recipient_info: fields_values['failure_type'] = recipient_info['failure_type'] - if state == 'sent': - self.assertMailMailWEmails([email], 'sent', content, fields_values=fields_values) - elif state == 'replied': # replied imply something has been sent - self.assertMailMailWEmails([email], 'sent', content, fields_values=fields_values) - elif state == 'ignored': - self.assertMailMailWEmails([email], 'cancel', content, fields_values=fields_values) - elif state == 'exception': - self.assertMailMailWEmails([email], 'exception', content, fields_values=fields_values) - elif state == 'canceled': - self.assertMailMailWEmails([email], 'canceled', content, fields_values=fields_values) + if partner: + self.assertMailMail(partner, state_mapping[state], author=author, content=content, fields_values=fields_values) else: - raise NotImplementedError() + self.assertMailMailWEmails([email], state_mapping[state], author=author, content=content, fields_values=fields_values) class MassMailCommon(MailCommon, MassMailCase): diff --git a/addons/mass_mailing/tests/test_mailing_internals.py b/addons/mass_mailing/tests/test_mailing_internals.py index 4639815bbb7..872161e16c3 100644 --- a/addons/mass_mailing/tests/test_mailing_internals.py +++ b/addons/mass_mailing/tests/test_mailing_internals.py @@ -240,8 +240,8 @@ class TestMassMailFeatures(MassMailCommon, CronMixinCase): mailing._process_mass_mailing_queue() self.assertMailTraces( - [{'email': 'test1@example.com'}, - {'email': 'test2@example.com', 'state': 'ignored'}], + [{'partner': partner_a}, + {'partner': partner_b, 'state': 'ignored'}], mailing, partner_a | partner_b, check_mail=True ) diff --git a/addons/sms/tests/test_sms_template.py b/addons/sms/tests/test_sms_template.py index b5a67655180..461b060f33a 100644 --- a/addons/sms/tests/test_sms_template.py +++ b/addons/sms/tests/test_sms_template.py @@ -3,8 +3,9 @@ from odoo.tests.common import TransactionCase, users from odoo.addons.mail.tests.common import mail_new_test_user -from odoo.tests import tagged from odoo.exceptions import AccessError +from odoo.tests import tagged +from odoo.tools import mute_logger @tagged('post_install') @@ -26,7 +27,8 @@ class TestSmsTemplateAccessRights(TransactionCase): cls.sms_templates = cls.env['sms.template'].create(vals) @users('user_employee') - def test_access_rights_user_sms_template(self): + @mute_logger('odoo.models.unlink') + def test_access_rights_user(self): # Check if a member of group_user can only read on sms.template for sms_template in self.env['sms.template'].browse(self.sms_templates.ids): self.assertTrue(bool(sms_template.name)) @@ -42,7 +44,8 @@ class TestSmsTemplateAccessRights(TransactionCase): sms_template.unlink() @users('user_system') - def test_access_rights_manager_sms_template(self): + @mute_logger('odoo.models.unlink', 'odoo.addons.base.models.ir_model') + def test_access_rights_system(self): admin = self.env.ref('base.user_admin') for sms_template in self.env['sms.template'].browse(self.sms_templates.ids): self.assertTrue(bool(sms_template.name)) diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py index bd72e0bf019..15d441d888c 100644 --- a/addons/test_mail/tests/__init__.py +++ b/addons/test_mail/tests/__init__.py @@ -10,7 +10,6 @@ from . import test_mail_mail from . import test_mail_gateway from . import test_mail_template from . import test_mail_thread_internals -from . import test_message_composer from . import test_message_management from . import test_message_post from . import test_message_track diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index 589f4105176..0547caca98d 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -3,7 +3,10 @@ import base64 +from unittest.mock import patch + from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.test_mail.models.test_mail_models import MailTestTicket from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients from odoo.tests import tagged from odoo.tests.common import users, Form @@ -18,9 +21,14 @@ class TestMailComposer(TestMailCommon, TestRecipients): super(TestMailComposer, cls).setUpClass() cls._init_mail_gateway() + # ensure employee can create partners, necessary for templates + cls.user_employee.write({ + 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)], + }) + cls.user_employee_2 = mail_new_test_user( cls.env, login='employee2', groups='base.group_user', - notification_type='inbox', email='eglantine@example.com', + notification_type='email', email='eglantine@example.com', name='Eglantine Employee', signature='--\nEglantine') cls.partner_employee_2 = cls.user_employee_2.partner_id @@ -37,6 +45,7 @@ class TestMailComposer(TestMailCommon, TestRecipients): 'report_type': 'qweb-pdf', 'report_name': 'test_mail.mail_test_ticket_test_template', }) + cls.test_record_report = cls.test_report._render_qweb_pdf(cls.test_report.ids) cls.test_from = '"John Doe" ' @@ -54,9 +63,10 @@ class TestMailComposer(TestMailCommon, TestRecipients): 'body_html': '

TemplateBody ${object.name}

', 'partner_to': '${object.customer_id.id if object.customer_id else ""}', 'email_to': '${(object.email_from if not object.customer_id else "") | safe}', - 'email_from': '${object.user_id.email_formatted | safe}', + 'email_from': '${(object.user_id.email_formatted or user.email_formatted) | safe}', 'model_id': cls.env['ir.model']._get('mail.test.ticket').id, 'mail_server_id': cls.mail_server.id, + 'auto_delete': True, }) def _generate_attachments_data(self, count): @@ -102,7 +112,59 @@ class TestComposerForm(TestMailComposer): # self.assertEqual(composer_form.record_name, self.test_record.name, 'MailComposer: comment mode should compute record name') self.assertFalse(composer_form.no_auto_thread) self.assertEqual(composer_form.composition_mode, 'comment') - self.assertEqual(composer_form.model, self.test_records._name) + self.assertEqual(composer_form.model, self.test_record._name) + + @users('employee') + def test_mail_composer_comment_attachments(self): + """Tests that all attachments are added to the composer, static attachments + are not duplicated and while reports are re-generated, and that intermediary + attachments are dropped.""" + attachment_data = self._generate_attachments_data(2) + template_1 = self.template.copy({ + 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'report_name': 'TestReport for ${object.name}.html', # test cursor forces html + 'report_template': self.test_report.id, + }) + template_1_attachments = template_1.attachment_ids + self.assertEqual(len(template_1_attachments), 2) + template_2 = self.template.copy({ + 'attachment_ids': False, + 'report_template': self.test_report.id, + }) + + # begins without attachments + composer_form = Form(self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=True, default_attachment_ids=[]) + )) + self.assertEqual(len(composer_form.attachment_ids), 0) + + # change template: 2 static (attachment_ids) and 1 dynamic (report) + composer_form.template_id = template_1 + self.assertEqual(len(composer_form.attachment_ids), 3) + report_attachments = [att for att in composer_form.attachment_ids if att not in template_1_attachments] + self.assertEqual(len(report_attachments), 1) + tpl_attachments = composer_form.attachment_ids[:] - report_attachments[0] + self.assertEqual(tpl_attachments, template_1_attachments) + + # change template: 0 static (attachment_ids) and 1 dynamic (report) + composer_form.template_id = template_2 + self.assertEqual(len(composer_form.attachment_ids), 1) + report_attachments = [att for att in composer_form.attachment_ids if att not in template_1_attachments] + self.assertEqual(len(report_attachments), 1) + tpl_attachments = composer_form.attachment_ids[:] - report_attachments[0] + self.assertEqual(tpl_attachments, self.env['ir.attachment']) + + # change back to template 1 + composer_form.template_id = template_1 + self.assertEqual(len(composer_form.attachment_ids), 3) + report_attachments = [att for att in composer_form.attachment_ids if att not in template_1_attachments] + self.assertEqual(len(report_attachments), 1) + tpl_attachments = composer_form.attachment_ids[:] - report_attachments[0] + self.assertEqual(tpl_attachments, template_1_attachments) + + # reset template + composer_form.template_id = self.env['mail.template'] + self.assertEqual(len(composer_form.attachment_ids), 0) @users('employee') def test_mail_composer_mass(self): @@ -139,6 +201,7 @@ class TestComposerInternals(TestMailComposer): attachment_data = self._generate_attachments_data(3) self.template.write({ 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'report_name': 'TestReport for ${object.name}.html', # test cursor forces html 'report_template': self.test_report.id, }) attachs = self.env['ir.attachment'].search([('name', 'in', [a['name'] for a in attachment_data])]) @@ -161,6 +224,7 @@ class TestComposerInternals(TestMailComposer): self.assertIn(attach, composer.attachment_ids) generated = composer.attachment_ids - attachs self.assertEqual(len(generated), 1, 'MailComposer: should have 1 additional attachment for report') + self.assertEqual(generated.name, 'TestReport for %s.html' % self.test_record.name) self.assertEqual(generated.res_model, 'mail.compose.message') self.assertEqual(generated.res_id, 0) @@ -207,9 +271,9 @@ class TestComposerInternals(TestMailComposer): self.assertEqual(composer.email_from, self.test_from) @users('employee') - def test_mail_composer_content_comment(self): - """ Test content management (subject, body, server) in comment mode. - Template update is also tested. """ + def test_mail_composer_content(self): + """ Test content management (subject, body, server) in both comment and + mass mailing mode. Template update is also tested. """ for composition_mode in ['comment', 'mass_mail']: if composition_mode == 'comment': ctx = self._get_web_context(self.test_record, add_web=False) @@ -314,3 +378,423 @@ class TestComposerInternals(TestMailComposer): self.assertEqual(composer.subject, 'My amazing subject') self.assertEqual(composer.body, '

Test Body

') self.assertEqual(composer.mail_server_id.id, False) + + @users('employee') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_parent(self): + """ Test specific management in comment mode when having parent_id set: + record_name, subject, parent's partners. """ + parent = self.test_record.message_post(body='Test', partner_ids=(self.partner_1 + self.partner_2).ids) + + composer = self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=False, default_parent_id=parent.id) + ).create({ + 'body': '

Test Body

', + }) + + # creation values taken from parent + self.assertEqual(composer.subject, 'Re: %s' % self.test_record.name) + self.assertEqual(composer.body, '

Test Body

') + self.assertEqual(composer.partner_ids, self.partner_1 + self.partner_2) + + def test_mail_composer_rights_portal(self): + portal_user = self._create_portal_user() + + with patch.object(MailTestTicket, 'check_access_rights', return_value=True): + self.env['mail.compose.message'].with_user(portal_user).with_context( + self._get_web_context(self.test_record) + ).create({ + 'subject': 'Subject', + 'body': '

Body text

', + 'partner_ids': [] + }).send_mail() + + self.assertEqual(self.test_record.message_ids[0].body, '

Body text

') + self.assertEqual(self.test_record.message_ids[0].author_id, portal_user.partner_id) + + self.env['mail.compose.message'].with_user(portal_user).with_context({ + 'default_composition_mode': 'comment', + 'default_parent_id': self.test_record.message_ids.ids[0], + }).create({ + 'subject': 'Subject', + 'body': '

Body text 2

' + }).send_mail() + + self.assertEqual(self.test_record.message_ids[0].body, '

Body text 2

') + self.assertEqual(self.test_record.message_ids[0].author_id, portal_user.partner_id) + + @users('employee') + def test_mail_composer_save_template(self): + self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record, add_web=False) + ).create({ + 'subject': 'Template Subject', + 'body': '

Template Body

', + }).save_as_template() + + # Test: email_template subject, body_html, model + template = self.env['mail.template'].search([ + ('model', '=', self.test_record._name), + ('subject', '=', 'Template Subject') + ], limit=1) + self.assertEqual(template.name, "%s: %s" % (self.env['ir.model']._get(self.test_record._name).name, 'Template Subject')) + self.assertEqual(template.body_html, '

Template Body

', 'email_template incorrect body_html') + + +@tagged('mail_composer') +class TestComposerResultsComment(TestMailComposer): + """ Test global output of composer used in comment mode. Test notably + notification and emails generated during this process. """ + + @users('employee') + @mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_recipients(self): + """ Test partner_ids given to composer are given to the final message. """ + composer = self.env['mail.compose.message'].with_context( + self._get_web_context(self.test_record) + ).create({ + 'body': '

Test Body

', + 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)] + }) + composer.send_mail() + + message = self.test_record.message_ids[0] + self.assertEqual(message.body, '

Test Body

') + self.assertEqual(message.author_id, self.user_employee.partner_id) + self.assertEqual(message.subject, 'Re: %s' % self.test_record.name) + self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment')) + self.assertEqual(message.partner_ids, self.partner_1 | self.partner_2) + + @users('employee') + @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. + + This tests notifies: 2 new email_to (+ 1 duplicated), 1 email_cc, + test_record followers and partner_admin added in partner_to.""" + attachment_data = self._generate_attachments_data(2) + email_to_1 = 'test.to.1@test.example.com' + email_to_2 = 'test.to.2@test.example.com' + email_to_3 = 'test.to.1@test.example.com' # duplicate: should not sent twice the email + email_cc_1 = 'test.cc.1@test.example.com' + self.template.write({ + 'auto_delete': False, # keep sent emails to check content + 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'email_to': '%s, %s, %s' % (email_to_1, email_to_2, email_to_3), + 'email_cc': email_cc_1, + 'partner_to': '%s, ${object.customer_id.id if object.customer_id else ""}' % self.partner_admin.id, + 'report_name': 'TestReport for ${object.name}', # test cursor forces html + 'report_template': self.test_report.id, + }) + 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) + + # 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() + with self.mock_mail_gateway(mail_unlink_sent=False), self.mock_mail_app(): + composer.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']) + ) + + # 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') + + # 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': '00.txt', 'raw': b'Att00', 'type': 'text/plain'}, + {'name': '01.txt', 'raw': b'Att01', 'type': 'text/plain'}, + {'name': 'TestReport for %s.html' % self.test_record.name, 'type': 'text/plain'}, + ] + }, + fields_values={}, + ) + 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': '00.txt', 'raw': b'Att00', 'type': 'text/plain'}, + {'name': '01.txt', 'raw': b'Att01', 'type': 'text/plain'}, + {'name': 'TestReport for %s.html' % self.test_record.name, 'type': 'text/plain'}, + ] + }, + fields_values={}, + ) + + # 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(['00.txt', '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') + + +@tagged('mail_composer') +class TestComposerResultsMass(TestMailComposer): + + @classmethod + def setUpClass(cls): + super(TestComposerResultsMass, cls).setUpClass() + # ensure employee can create partners, necessary for templates + cls.user_employee.write({ + 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)], + }) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl(self): + self.template.auto_delete = False # keep sent emails to check content + 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): + composer.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') + + for record in self.test_records: + # message copy is kept + message = record.message_ids[0] + + # template is sent directly using customer field, meaning we have recipients + self.assertMailMail(record.customer_id, 'sent', + mail_message=message, + author=self.partner_employee) + + # message content + self.assertEqual(message.subject, 'TemplateSubject %s' % record.name) + self.assertEqual(message.body, '

TemplateBody %s

' % record.name) + self.assertEqual(message.author_id, self.user_employee.partner_id) + # post-related fields are void + self.assertEqual(message.subtype_id, self.env['mail.message.subtype']) + self.assertEqual(message.partner_ids, self.env['res.partner']) + + @users('employee') + @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. """ + attachment_data = self._generate_attachments_data(2) + email_to_1 = 'test.to.1@test.example.com' + email_to_2 = 'test.to.2@test.example.com' + email_to_3 = 'test.to.1@test.example.com' # duplicate: should not sent twice the email + email_cc_1 = 'test.cc.1@test.example.com' + self.template.write({ + 'auto_delete': False, # keep sent emails to check content + 'attachment_ids': [(0, 0, a) for a in attachment_data], + 'email_to': '%s, %s, %s' % (email_to_1, email_to_2, email_to_3), + 'email_cc': email_cc_1, + 'partner_to': '%s, ${object.customer_id.id if object.customer_id else ""}' % self.partner_admin.id, + 'report_name': 'TestReport for ${object.name}', # test cursor forces html + 'report_template': self.test_report.id, + }) + 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_records.user_id, self.env['res.users']) + self.assertEqual(self.test_records.message_partner_ids, self.env['res.partner']) + + # 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() + with self.mock_mail_gateway(mail_unlink_sent=False): + composer.send_mail() + + 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) + + # global outgoing + self.assertEqual(len(self._new_mails), 2, 'Should have created 1 mail.mail per record') + self.assertEqual(len(self._mails), 10, 'Should have sent 5 emails per record') + + # 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 + ] + _mails_record2 = [mail for mail in self._mails if '%s-%s' % (self.test_records[1].id, self.test_records._name) in mail['message_id']] + + for record, _mails in zip(self.test_records, _mails_records): + # message copy is kept + message = record.message_ids[0] + + # 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=self.partner_employee, + email_values={ + 'body_content': 'TemplateBody %s' % record.name, + 'subject': 'TemplateSubject %s' % record.name, + # 'attachments': [ + # ('00.txt', b'Att00', 'text/plain'), + # ('01.txt', b'Att01', 'text/plain'), + # ('report.test_mail.mail_test_ticket_test_template.html', b'My second attachment', 'text/plain') + # ] + } + ) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_delete(self): + self.template.auto_delete = True + 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): + composer.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(self._new_mails.exists(), self.env['mail.mail'], 'Should have deleted mail.mail records') + + for record in self.test_records: + # message copy is kept + message = record.message_ids[0] + + # template is sent directly using customer field + self.assertSentEmail(self.partner_employee, record.customer_id) + + # message content + self.assertEqual(message.subject, 'TemplateSubject %s' % record.name) + self.assertEqual(message.body, '

TemplateBody %s

' % record.name) + self.assertEqual(message.author_id, self.user_employee.partner_id) + # post-related fields are void + self.assertEqual(message.subtype_id, self.env['mail.message.subtype']) + self.assertEqual(message.partner_ids, self.env['res.partner']) + + @users('employee') + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_mail_composer_wtpl_delete_notif(self): + self.template.auto_delete = True + 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, + default_auto_delete_message=True) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.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(self._new_mails.exists(), self.env['mail.mail'], 'Should have deleted mail.mail records') + + for record in self.test_records: + # message copy is unlinked + self.assertEqual(record.message_ids, self.env['mail.message'], 'Should have deleted mail.message records') + + # template is sent directly using customer field + self.assertSentEmail(self.partner_employee, record.customer_id) + + @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, + active_ids, ... to ensure fallback behavior are working. """ + # 1: active_domain + 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)]) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.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') + + for record in self.test_records: + # template is sent directly using customer field + self.assertSentEmail(self.partner_employee, record.customer_id) + + # 2: active_domain not taken into account if use_active_domain is False + 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, + default_use_active_domain=False, + default_active_domain=[('id', 'in', -1)]) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=True): + composer.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') + + # 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=[]) + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False): + composer.send_mail() + + # global outgoing + 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 + composer_form = Form(self.env['mail.compose.message'].with_context( + default_model='mail.test.ticket', + default_template_id=self.template.id + )) + composer = composer_form.save() + with self.mock_mail_gateway(mail_unlink_sent=False), self.assertRaises(ValueError): + composer.send_mail() diff --git a/addons/test_mail/tests/test_message_composer.py b/addons/test_mail/tests/test_message_composer.py deleted file mode 100644 index ea6aa91803f..00000000000 --- a/addons/test_mail/tests/test_message_composer.py +++ /dev/null @@ -1,345 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import base64 - -from unittest.mock import patch - -from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients -from odoo.addons.test_mail.models.test_mail_models import MailTestSimple -from odoo.tests import tagged -from odoo.tools import mute_logger - - -@tagged('mail_composer') -class TestComposer(TestMailCommon, TestRecipients): - """ Test Composer integration and results """ - - @classmethod - def setUpClass(cls): - super(TestComposer, cls).setUpClass() - cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) - cls._reset_mail_context(cls.test_record) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_composer_comment(self): - composer = self.env['mail.compose.message'].with_context({ - 'default_composition_mode': 'comment', - 'default_model': self.test_record._name, - 'default_res_id': self.test_record.id, - }).with_user(self.user_employee).create({ - 'body': '

Test Body

', - 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)] - }) - composer.send_mail() - - message = self.test_record.message_ids[0] - self.assertEqual(message.body, '

Test Body

') - self.assertEqual(message.author_id, self.user_employee.partner_id) - self.assertEqual(message.subject, 'Re: %s' % self.test_record.name) - self.assertEqual(message.subtype_id, self.env.ref('mail.mt_comment')) - self.assertEqual(message.partner_ids, self.partner_1 | self.partner_2) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_composer_comment_parent(self): - parent = self.test_record.message_post(body='Test') - - self.env['mail.compose.message'].with_context({ - 'default_composition_mode': 'comment', - 'default_parent_id': parent.id - }).with_user(self.user_employee).create({ - 'body': '

Mega

', - }).send_mail() - - message = self.test_record.message_ids[0] - self.assertEqual(message.body, '

Mega

') - self.assertEqual(message.parent_id, parent) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_composer_mass_mail(self): - test_record_2 = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test2'}) - - composer = self.env['mail.compose.message'].with_context({ - 'default_composition_mode': 'mass_mail', - 'default_model': self.test_record._name, - 'default_res_id': False, - 'active_ids': [self.test_record.id, test_record_2.id] - }).with_user(self.user_employee).create({ - 'subject': 'Testing ${object.name}', - 'body': '

${object.name}

', - 'partner_ids': [(4, self.partner_1.id), (4, self.partner_2.id)] - }) - composer.with_context({ - 'default_res_id': -1, - 'active_ids': [self.test_record.id, test_record_2.id] - }).send_mail() - - # check mail_mail - mails = self.env['mail.mail'].sudo().search([('subject', 'ilike', 'Testing')]) - for mail in mails: - self.assertEqual(mail.recipient_ids, self.partner_1 | self.partner_2, - 'compose wizard: mail_mail mass mailing: mail.mail in mass mail incorrect recipients') - - # check message on test_record - message1 = self.test_record.message_ids[0] - self.assertEqual(message1.subject, 'Testing %s' % self.test_record.name) - self.assertEqual(message1.body, '

%s

' % self.test_record.name) - - # check message on test_record_2 - message1 = test_record_2.message_ids[0] - self.assertEqual(message1.subject, 'Testing %s' % test_record_2.name) - self.assertEqual(message1.body, '

%s

' % test_record_2.name) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_composer_mass_mail_active_domain(self): - test_record_2 = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test2'}) - - self.env['mail.compose.message'].with_context({ - 'default_composition_mode': 'mass_mail', - 'default_model': self.test_record._name, - 'default_use_active_domain': True, - 'active_ids': [self.test_record.id], - 'active_domain': [('name', 'in', ['%s' % self.test_record.name, '%s' % test_record_2.name])], - }).with_user(self.user_employee).create({ - 'subject': 'From Composer Test', - 'body': '${object.name}', - }).send_mail() - - self.assertEqual(self.test_record.message_ids[0].subject, 'From Composer Test') - self.assertEqual(test_record_2.message_ids[0].subject, 'From Composer Test') - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_message_compose_mass_mail_no_active_domain(self): - test_record_2 = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test2'}) - - self.env['mail.compose.message'].with_context({ - 'default_composition_mode': 'mass_mail', - 'default_model': self.test_record._name, - 'default_use_active_domain': False, - 'active_ids': [self.test_record.id], - 'active_domain': [('name', 'in', ['%s' % self.test_record.name, '%s' % test_record_2.name])], - }).with_user(self.user_employee).create({ - 'subject': 'From Composer Test', - 'body': '${object.name}', - }).send_mail() - - self.assertEqual(self.test_record.message_ids[0].subject, 'From Composer Test') - self.assertFalse(test_record_2.message_ids.ids) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_message_compose_portal_ok(self): - portal_user = self._create_portal_user() - - with patch.object(MailTestSimple, 'check_access_rights', return_value=True): - ComposerPortal = self.env['mail.compose.message'].with_user(portal_user) - - ComposerPortal.with_context({ - 'default_composition_mode': 'comment', - 'default_model': self.test_record._name, - 'default_res_id': self.test_record.id, - }).create({ - 'subject': 'Subject', - 'body': '

Body text

', - 'partner_ids': []}).send_mail() - - self.assertEqual(self.test_record.message_ids[0].body, '

Body text

') - self.assertEqual(self.test_record.message_ids[0].author_id, portal_user.partner_id) - - ComposerPortal.with_context({ - 'default_composition_mode': 'comment', - 'default_parent_id': self.test_record.message_ids.ids[0], - }).create({ - 'subject': 'Subject', - 'body': '

Body text 2

'}).send_mail() - - self.assertEqual(self.test_record.message_ids[0].body, '

Body text 2

') - self.assertEqual(self.test_record.message_ids[0].author_id, portal_user.partner_id) - - -@tagged('mail_composer') -class TestComposerWTpl(TestMailCommon, TestRecipients): - - @classmethod - def setUpClass(cls): - super(TestComposerWTpl, cls).setUpClass() - cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) - - cls.user_employee.write({ - 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)], - }) - - cls._attachments = [{ - 'name': 'first.txt', - 'datas': base64.b64encode(b'My first attachment'), - 'res_model': 'res.partner', - 'res_id': cls.user_admin.partner_id.id - }, { - 'name': 'second.txt', - 'datas': base64.b64encode(b'My second attachment'), - 'res_model': 'res.partner', - 'res_id': cls.user_admin.partner_id.id - }] - - cls.email_1 = 'test1@example.com' - cls.email_2 = 'test2@example.com' - cls.email_3 = cls.partner_1.email - cls._create_template('mail.test.simple', { - 'attachment_ids': [(0, 0, cls._attachments[0]), (0, 0, cls._attachments[1])], - 'partner_to': '%s,%s' % (cls.partner_2.id, cls.user_admin.partner_id.id), - 'email_to': '%s, %s' % (cls.email_1, cls.email_2), - 'email_cc': '%s' % cls.email_3, - }) - - # admin should receive emails - cls.user_admin.write({'notification_type': 'email'}) - # Force the attachments of the template to be in the natural order. - cls.email_template.invalidate_cache(['attachment_ids'], ids=cls.email_template.ids) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_composer_w_template(self): - composer = self.env['mail.compose.message'].with_user(self.user_employee).with_context({ - 'default_composition_mode': 'comment', - 'default_model': 'mail.test.simple', - 'default_res_id': self.test_record.id, - 'default_template_id': self.email_template.id, - }).create({'subject': 'Forget me subject', 'body': 'Dummy body'}) - - # perform onchange and send emails - values = composer.onchange_template_id(self.email_template.id, 'comment', self.test_record._name, self.test_record.id)['value'] - composer.write(values) - with self.mock_mail_gateway(mail_unlink_sent=True): - composer.send_mail() - - new_partners = self.env['res.partner'].search([('email', 'in', [self.email_1, self.email_2])]) - self.assertMailMail( - [self.partner_1, self.partner_2, new_partners[0], new_partners[1], self.partner_admin], - 'sent', - author=self.user_employee.partner_id, - mail_message=False, - check_mail_mail=False, - email_values={ - 'subject': 'About %s' % self.test_record.name, - 'body_content': self.test_record.name, - 'attachments': [ - ('first.txt', b'My first attachment', 'text/plain'), - ('second.txt', b'My second attachment', 'text/plain') - ] - } - ) - - def test_composer_template_onchange_attachments(self): - """Tests that all attachments are added to the composer, - static attachments are not duplicated and while reports are re-generated, - and that intermediary attachments are dropped.""" - - composer = self.env['mail.compose.message'].with_context(default_attachment_ids=[]).create({}) - report_template = self.env.ref('web.action_report_externalpreview') - template_1 = self.email_template.copy({ - 'report_template': report_template.id, - }) - template_2 = self.email_template.copy({ - 'attachment_ids': False, - 'report_template': report_template.id, - }) - - onchange_templates = [template_1, template_2, template_1, False] - attachments_onchange = [composer.attachment_ids] - # template_1 has two static attachments and one dynamically generated report, - # template_2 only has the report, so we should get 3, 1, 3 attachments - attachment_numbers = [0, 3, 1, 3, 0] - - for template in onchange_templates: - onchange = composer.onchange_template_id( - template.id if template else False, 'comment', self.test_record._name, self.test_record.id - ) - composer.update(onchange['value']) - attachments_onchange.append(composer.attachment_ids) - - self.assertEqual( - [len(attachments) for attachments in attachments_onchange], - attachment_numbers, - ) - - self.assertTrue( - len(attachments_onchange[1] & attachments_onchange[3]) == 2, - "The two static attachments on the template should be common to the two onchanges" - ) - - @mute_logger('odoo.addons.mail.models.mail_mail') - def test_composer_w_template_mass_mailing(self): - test_record_2 = self.env['mail.test.simple'].with_context(self._test_context).create({'name': 'Test2', 'email_from': 'laurie.poiret@example.com'}) - - composer = self.env['mail.compose.message'].with_user(self.user_employee).with_context({ - 'default_composition_mode': 'mass_mail', - # 'default_notify': True, - 'default_notify': False, - 'default_model': 'mail.test.simple', - 'default_res_id': self.test_record.id, - 'default_template_id': self.email_template.id, - 'active_ids': [self.test_record.id, test_record_2.id] - }).create({}) - values = composer.onchange_template_id(self.email_template.id, 'mass_mail', 'mail.test.simple', self.test_record.id)['value'] - composer.write(values) - with self.mock_mail_gateway(mail_unlink_sent=True): - composer.send_mail() - - new_partners = self.env['res.partner'].search([('email', 'in', [self.email_1, self.email_2])]) - # hack to use assertEmails - self._mails_record1 = [dict(mail) for mail in self._mails if '%s-%s' % (self.test_record.id, self.test_record._name) in mail['message_id']] - self._mails_record2 = [dict(mail) for mail in self._mails if '%s-%s' % (test_record_2.id, test_record_2._name) in mail['message_id']] - - self._mails = self._mails_record1 - self.assertMailMail( - [self.partner_1, self.partner_2, new_partners[0], new_partners[1], self.partner_admin], - 'sent', - author=self.user_employee.partner_id, - mail_message=False, - check_mail_mail=False, - email_values={ - 'subject': 'About %s' % self.test_record.name, - 'body_content': self.test_record.name, - 'attachments': [ - ('first.txt', b'My first attachment', 'text/plain'), - ('second.txt', b'My second attachment', 'text/plain') - ] - } - ) - - self._mails = self._mails_record2 - self.assertMailMail( - [self.partner_1, self.partner_2, new_partners[0], new_partners[1], self.partner_admin], - 'sent', - author=self.user_employee.partner_id, - mail_message=False, - check_mail_mail=False, - email_values={ - 'subject': 'About %s' % test_record_2.name, - 'body_content': test_record_2.name, - 'attachments': [ - ('first.txt', b'My first attachment', 'text/plain'), - ('second.txt', b'My second attachment', 'text/plain') - ] - } - ) - - message_1 = self.test_record.message_ids[0] - message_2 = test_record_2.message_ids[0] - - # messages effectively posted - self.assertEqual(message_1.subject, 'About %s' % self.test_record.name) - self.assertEqual(message_2.subject, 'About %s' % test_record_2.name) - self.assertIn(self.test_record.name, message_1.body) - self.assertIn(test_record_2.name, message_2.body) - - def test_composer_template_save(self): - self.env['mail.compose.message'].with_context({ - 'default_composition_mode': 'comment', - 'default_model': 'mail.test.simple', - 'default_res_id': self.test_record.id, - }).create({ - 'subject': 'Forget me subject', - 'body': '

Dummy body

' - }).save_as_template() - # Test: email_template subject, body_html, model - last_template = self.env['mail.template'].search([('model', '=', 'mail.test.simple'), ('subject', '=', 'Forget me subject')], limit=1) - self.assertEqual(last_template.body_html, '

Dummy body

', 'email_template incorrect body_html') diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py index db0c27812dd..c696fb0a263 100644 --- a/addons/test_mail/tests/test_message_post.py +++ b/addons/test_mail/tests/test_message_post.py @@ -113,10 +113,12 @@ class TestMessagePost(TestMailCommon, TestRecipients): # subscribe second employee to the group to test notifications self.test_record.message_subscribe(partner_ids=[self.user_admin.partner_id.id]) - with self.assertSinglePostNotifications([ - {'partner': self.partner_1, 'type': 'email'}, - {'partner': self.partner_2, 'type': 'email'}, - {'partner': self.partner_admin, 'type': 'email'}], {'content': _body}, mail_unlink_sent=True): + with self.assertSinglePostNotifications( + [{'partner': self.partner_1, 'type': 'email'}, + {'partner': self.partner_2, 'type': 'email'}, + {'partner': self.partner_admin, 'type': 'email'}], + {'content': _body}, + mail_unlink_sent=True): msg = self.test_record.with_user(self.user_employee).message_post( body=_body, subject=_subject, message_type='comment', subtype_xmlid='mail.mt_comment', diff --git a/addons/test_mail_full/tests/test_mass_mailing.py b/addons/test_mail_full/tests/test_mass_mailing.py index 2745855cea8..693022572fa 100644 --- a/addons/test_mail_full/tests/test_mass_mailing.py +++ b/addons/test_mail_full/tests/test_mass_mailing.py @@ -75,6 +75,6 @@ class TestMassMailing(TestMailFullCommon): 'http://localhost:%s/mailing/%s/view' % (config['http_port'], mailing.id), email['body']) - self.assertMailTraces([recipient_info], mailing, recipient, check_mail=True) + self.assertMailTraces([recipient_info], mailing, recipient, check_mail=True, author=self.env.user.partner_id) self.assertEqual(mailing.ignored, 4) diff --git a/addons/test_mass_mailing/tests/common.py b/addons/test_mass_mailing/tests/common.py index 84f04bb29b1..2a550878b12 100644 --- a/addons/test_mass_mailing/tests/common.py +++ b/addons/test_mass_mailing/tests/common.py @@ -14,7 +14,9 @@ class TestMassMailCommon(MassMailCommon, TestMailCommon): def setUpClass(cls): super(TestMassMailCommon, cls).setUpClass() - cls.mailing_bl = cls.env['mailing.mailing'].create({ + # enforce last update by user_marketing to match _process_mass_mailing_queue + # taking last writer as user running a batch + cls.mailing_bl = cls.env['mailing.mailing'].with_user(cls.user_marketing).create({ 'name': 'SourceName', 'subject': 'MailingSubject', 'body_html': '

Hello ${object.name}

', diff --git a/addons/test_mass_mailing/tests/test_blacklist_behavior.py b/addons/test_mass_mailing/tests/test_blacklist_behavior.py index aa513d2a389..e07bfff760c 100644 --- a/addons/test_mass_mailing/tests/test_blacklist_behavior.py +++ b/addons/test_mass_mailing/tests/test_blacklist_behavior.py @@ -35,7 +35,8 @@ class TestAutoBlacklist(common.TestMassMailCommon): self.assertMailTraces( [{'email': 'test.record.00@test.example.com'}], - mailing, target, check_mail=True + mailing, target, + check_mail=True ) # call bounced