diff --git a/addons/hr_recruitment/wizard/applicant_send_mail.py b/addons/hr_recruitment/wizard/applicant_send_mail.py index de3fc005851..79e6d884d4b 100644 --- a/addons/hr_recruitment/wizard/applicant_send_mail.py +++ b/addons/hr_recruitment/wizard/applicant_send_mail.py @@ -41,10 +41,10 @@ class ApplicantSendMail(models.TransientModel): }) applicant.message_post( - subject=self.subject, body=self.body, - message_type='comment', email_from=self.author_id.email, email_layout_xmlid='mail.mail_notification_light', + message_type='comment', partner_ids=applicant.partner_id.ids, + subject=self.subject, ) diff --git a/addons/mail/models/mail_render_mixin.py b/addons/mail/models/mail_render_mixin.py index 39874b7b25c..8ad6fe4a913 100644 --- a/addons/mail/models/mail_render_mixin.py +++ b/addons/mail/models/mail_render_mixin.py @@ -13,7 +13,7 @@ from werkzeug import urls from odoo import _, api, fields, models, tools from odoo.addons.base.models.ir_qweb import QWebException from odoo.exceptions import UserError, AccessError -from odoo.tools import is_html_empty, safe_eval +from odoo.tools import is_html_empty from odoo.tools.rendering_tools import convert_inline_template_to_qweb, parse_inline_template, render_inline_template, template_env_globals _logger = logging.getLogger(__name__) @@ -273,6 +273,10 @@ class MailRenderMixin(models.AbstractModel): add_context=None, options=None): """ Render a raw QWeb template. + In addition to the generic evaluation context available, some other + variables are added: + * ``object``: record based on which the template is rendered; + :param str template_src: raw QWeb template to render; :param str model: see ``MailRenderMixin._render_template()``; :param list res_ids: see ``MailRenderMixin._render_template()``; @@ -283,8 +287,6 @@ class MailRenderMixin(models.AbstractModel): :param dict options: options for rendering (not used currently); :return dict: {res_id: string of rendered template based on record} - - :notice: Experimental. Use at your own risks only. """ results = dict.fromkeys(res_ids, u"") if not template_src: @@ -473,6 +475,7 @@ class MailRenderMixin(models.AbstractModel): else: rendered = self._render_template_inline_template(template_src, model, res_ids, add_context=add_context, options=options) + if post_process: rendered = self._render_template_postprocess(rendered) @@ -543,6 +546,7 @@ class MailRenderMixin(models.AbstractModel): :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 dict add_context: additional context to give to renderer; :param dict options: options for rendering; :param boolean post_process: perform a post processing on rendered result @@ -570,7 +574,12 @@ class MailRenderMixin(models.AbstractModel): (res_id, rendered) for lang, (template, tpl_res_ids) in templates_res_ids.items() for res_id, rendered in template._render_template( - template[field], template.render_model, tpl_res_ids, engine=engine, - add_context=add_context, options=options, post_process=post_process + template[field], + template.render_model, + tpl_res_ids, + engine=engine, + add_context=add_context, + options=options, + post_process=post_process ).items() ) diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index 3939d56ae4c..920647e98c7 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -334,10 +334,16 @@ class MailTemplate(models.Model): # create a mail_mail based on values, without attachments values = self.generate_email( res_id, - ['subject', 'body_html', + ('auto_delete', + 'body_html', + 'email_cc', 'email_from', - 'email_cc', 'email_to', 'partner_to', 'reply_to', - 'auto_delete', 'scheduled_date'] + 'email_to', + 'partner_to', + 'reply_to', + 'scheduled_date', + 'subject', + ) ) values['recipient_ids'] = [Command.link(pid) for pid in values.get('partner_ids', list())] values['attachment_ids'] = [Command.link(aid) for aid in values.get('attachment_ids', list())] diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 1a8c7b837f5..df0afdd4cce 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1977,9 +1977,11 @@ class MailThread(models.AbstractModel): if not msg_values.get('record_name'): msg_values['record_name'] = self.display_name msg_values.update({ + # author 'author_id': author_id, 'author_guest_id': author_guest_id, 'email_from': email_from, + # document 'model': self._name, 'res_id': self.id, # content @@ -2118,18 +2120,22 @@ class MailThread(models.AbstractModel): res_id = False msg_values = { - 'parent_id': parent_id, - 'model': self._name if self else model, - 'res_id': self.id if self else res_id, - 'message_type': 'user_notification', - 'subject': subject, - 'body': body, + # author 'author_id': author_id, 'email_from': email_from, - 'partner_ids': partner_ids, - 'is_internal': True, + # document + 'model': self._name if self else model, 'record_name': False, + 'res_id': self.id if self else res_id, + # content + 'body': body, + 'is_internal': True, + 'message_type': 'user_notification', + 'parent_id': parent_id, + 'subject': subject, + # recipients 'message_id': tools.generate_tracking_message_id('message-notify'), + 'partner_ids': partner_ids, } msg_values.update(msg_kwargs) # add default-like values afterwards, to avoid useless queries @@ -2159,19 +2165,23 @@ class MailThread(models.AbstractModel): author_id, email_from = self._message_compute_author(author_id, email_from, raise_on_email=False) msg_values = { - 'subject': subject, - 'body': body, + # author 'author_id': author_id, 'email_from': email_from, - 'message_type': message_type, + # document 'model': kwargs.get('model', self._name), - 'res_id': self.ids[0] if self.ids else False, - 'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'), - 'is_internal': True, 'record_name': False, - 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False], - 'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify + 'res_id': self.ids[0] if self.ids else False, + # content + 'body': body, + 'is_internal': True, + 'message_type': message_type, + 'subject': subject, + 'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'), + # recipients 'email_add_signature': False, # False as no notification -> no need to compute signature + 'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify + 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False], } msg_values.update(kwargs) @@ -2186,17 +2196,21 @@ class MailThread(models.AbstractModel): author_id, email_from = self._message_compute_author(author_id, email_from, raise_on_email=False) base_message_values = { - 'subject': subject, + # author 'author_id': author_id, 'email_from': email_from, - 'message_type': message_type, + # document 'model': self._name, - 'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'), - 'is_internal': True, 'record_name': False, - 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False], - 'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify + # content + 'message_type': message_type, + 'is_internal': True, + 'subject': subject, + 'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'), + # recipients 'email_add_signature': False, + 'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify + 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False], } values_list = [dict(base_message_values, res_id=record.id, @@ -2408,9 +2422,9 @@ class MailThread(models.AbstractModel): notif_create_values = [{ 'author_id': message.author_id.id, 'mail_message_id': message.id, - 'res_partner_id': pid, - 'notification_type': 'inbox', 'notification_status': 'sent', + 'notification_type': 'inbox', + 'res_partner_id': pid, } for pid in inbox_pids] self.env['mail.notification'].sudo().create(notif_create_values) @@ -2531,12 +2545,12 @@ class MailThread(models.AbstractModel): }) notif_create_values += [{ 'author_id': message.author_id.id, - 'mail_message_id': message.id, - 'res_partner_id': recipient_id, - 'notification_type': 'email', - 'mail_mail_id': new_email.id, 'is_read': True, # discard Inbox notification + 'mail_mail_id': new_email.id, + 'mail_message_id': message.id, 'notification_status': 'ready', + 'notification_type': 'email', + 'res_partner_id': recipient_id, } for recipient_id in tocreate_recipient_ids] emails += new_email diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index d8061f99946..3912398d5c3 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -20,6 +20,7 @@ from odoo.addons.mail.models.mail_message import Message from odoo.addons.mail.models.mail_notification import MailNotification from odoo.tests import common, new_test_user from odoo.tools import formataddr, pycompat +from odoo.tools.translate import code_translations mail_new_test_user = partial(new_test_user, context={'mail_create_nolog': True, 'mail_create_nosubscribe': True, @@ -1119,9 +1120,16 @@ class MailCommon(common.TransactionCase, MailCase): if test_record: cls.env['ir.model']._get(test_record._name).with_context(lang=lang_code).name = 'Spanish description' + # Translate some code strings used in mailing + code_translations.python_translations[('test_mail', 'es_ES')] = {'TestStuff': 'TestSpanishStuff'} + cls.addClassCleanup(code_translations.python_translations.pop, ('test_mail', 'es_ES')) + code_translations.python_translations[('mail', 'es_ES')] = {'View %s': 'SpanishView %s'} + cls.addClassCleanup(code_translations.python_translations.pop, ('mail', 'es_ES')) + # Prepare some translated value for template if given - test_template.with_context(lang=lang_code).subject = 'SpanishSubject for {{ object.name }}' - test_template.with_context(lang=lang_code).body_html = '

SpanishBody for

' + if test_template: + test_template.with_context(lang=lang_code).subject = 'SpanishSubject for {{ object.name }}' + test_template.with_context(lang=lang_code).body_html = '

SpanishBody for

' # create a custom layout for email notification if not layout_arch_db: diff --git a/addons/mail/tests/test_mail_render.py b/addons/mail/tests/test_mail_render.py index acb7b45a02c..b196063043c 100644 --- a/addons/mail/tests/test_mail_render.py +++ b/addons/mail/tests/test_mail_render.py @@ -8,12 +8,11 @@ from odoo.exceptions import AccessError from odoo.tests import tagged, users -@tagged('mail_render') -class TestMailRender(common.MailCommon): +class TestMailRenderCommon(common.MailCommon): @classmethod def setUpClass(cls): - super(TestMailRender, cls).setUpClass() + super(TestMailRenderCommon, cls).setUpClass() # activate multi language support cls.env['res.lang']._activate_lang('fr_FR') @@ -40,10 +39,7 @@ class TestMailRender(common.MailCommon):

{{ 13 + 13 }}

This is a test

""", - """ - Test - {{ '' if True else 'Code not executed' }} - """, + """Test{{ '' if True else 'Code not executed' }}""", ] cls.base_inline_template_bits_fr = [ '

Bonjour

', @@ -136,6 +132,10 @@ class TestMailRender(common.MailCommon): ) cls.user_rendering_restricted.groups_id -= cls.env.ref('mail.group_mail_template_editor') + +@tagged('mail_render') +class TestMailRender(TestMailRenderCommon): + @users('employee') def test_evaluation_context(self): """ Test evaluation context and various ways of tweaking it. """ @@ -164,6 +164,15 @@ class TestMailRender(common.MailCommon): )[partner.id] self.assertEqual(expected, result) + @users('employee') + def test_prepend_preview_inline_template_to_qweb(self): + body = 'body' + preview = 'foo{{"false" if 1 > 2 else "true"}}bar' + result = self.env['mail.render.mixin']._prepend_preview(Markup(body), preview) + self.assertEqual(result, '''
+ foobar +
body''') + @users('employee') def test_render_field(self): template = self.env['mail.template'].browse(self.test_template.ids) @@ -176,6 +185,10 @@ class TestMailRender(common.MailCommon): )[partner.id] self.assertEqual(rendered, expected) + @users('employee') + def test_render_field_lang(self): + """ Test translation in french """ + template = self.env['mail.template'].browse(self.test_template.ids) 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( @@ -197,23 +210,6 @@ class TestMailRender(common.MailCommon): )[partner.id] self.assertEqual(rendered, expected) - @users('employee') - def test_render_template_local_links(self): - local_links_template_bits = [ - '
', - '
', - '
', - ] - base_url = self.env['mail.render.mixin'].get_base_url() - rendered_local_links = [ - '
' % base_url, - '
' % base_url, - '
' % base_url - ] - for source, expected in zip(local_links_template_bits, rendered_local_links): - rendered = self.env['mail.render.mixin']._replace_local_links(source) - self.assertEqual(rendered, expected) - @users('employee') def test_render_template_qweb(self): partner = self.env['res.partner'].browse(self.render_object.ids) @@ -239,77 +235,7 @@ class TestMailRender(common.MailCommon): self.assertEqual(rendered, expected) @users('employee') - def test_template_rendering_impersonate(self): - """ Test that the use of SUDO do not change the current user. """ - partner = self.env['res.partner'].browse(self.render_object.ids) - src = '{{ user.name }} - {{ object.name }}' - expected = '%s - %s' % (self.env.user.name, partner.name) - result = self.env['mail.render.mixin'].sudo()._render_template_inline_template( - src, partner._name, partner.ids - )[partner.id] - self.assertIn(expected, result) - - @users('user_rendering_restricted') - def test_template_rendering_function_call(self): - """Test the case when the template call a custom function. - - This function should not be called when the template is not rendered. - """ - model = 'res.partner' - res_ids = self.env[model].search([], limit=1).ids - partner = self.env[model].browse(res_ids) - MailRenderMixin = self.env['mail.render.mixin'] - - def cust_function(): - # Can not use "MagicMock" in a Jinja sand-boxed environment - # so create our own function - cust_function.call = True - return 'return value' - - cust_function.call = False - - src = """

This is a test

-

{{ cust_function() }}

""" - expected = """

This is a test

-

return value

""" - context = {'cust_function': cust_function} - - result = self.env['mail.render.mixin'].with_user(self.user_admin)._render_template_inline_template( - src, partner._name, partner.ids, - add_context=context - )[partner.id] - self.assertEqual(expected, result) - self.assertTrue(cust_function.call) - - with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'): - MailRenderMixin._render_template_inline_template(src, model, res_ids, add_context=context) - - @users('user_rendering_restricted') - def test_template_render_static(self): - """Test that we render correctly static templates (without placeholders).""" - model = 'res.partner' - res_ids = self.env[model].search([], limit=1).ids - MailRenderMixin = self.env['mail.render.mixin'] - - result = MailRenderMixin._render_template_inline_template(self.base_inline_template_bits[0], model, res_ids)[res_ids[0]] - self.assertEqual(result, self.base_inline_template_bits[0]) - - @users('user_rendering_restricted') - def test_template_rendering_restricted(self): - """Test if we correctly detect static template.""" - res_ids = self.env['res.partner'].search([], limit=1).ids - with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'): - self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[3], 'res.partner', res_ids) - - @users('employee') - def test_template_rendering_unrestricted(self): - """Test if we correctly detect static template.""" - res_ids = self.env['res.partner'].search([], limit=1).ids - result = self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[3], 'res.partner', res_ids)[res_ids[0]] - self.assertIn('26', result, 'Template Editor should be able to render inline_template code') - - @users('employee') - def test_template_rendering_various(self): + def test_render_template_various(self): """ Test static rendering """ partner = self.env['res.partner'].browse(self.render_object.ids) MailRenderMixin = self.env['mail.render.mixin'] @@ -376,6 +302,131 @@ class TestMailRender(common.MailCommon): )[partner.id] self.assertEqual(result, expected) + @users('employee') + def test_replace_local_links(self): + local_links_template_bits = [ + '
', + '
', + '
', + ] + base_url = self.env['mail.render.mixin'].get_base_url() + rendered_local_links = [ + '
' % base_url, + '
' % base_url, + '
' % base_url + ] + for source, expected in zip(local_links_template_bits, rendered_local_links): + rendered = self.env['mail.render.mixin']._replace_local_links(source) + self.assertEqual(rendered, expected) + + +@tagged('mail_render') +class TestMailRenderSecurity(TestMailRenderCommon): + """ Test security of rendering, based on qweb finding + restricted rendering + group usage. """ + + @users('employee') + def test_render_inline_template_impersonate(self): + """ Test that the use of SUDO do not change the current user. """ + partner = self.env['res.partner'].browse(self.render_object.ids) + src = '{{ user.name }} - {{ object.name }}' + expected = '%s - %s' % (self.env.user.name, partner.name) + result = self.env['mail.render.mixin'].sudo()._render_template_inline_template( + src, partner._name, partner.ids + )[partner.id] + self.assertIn(expected, result) + + @users('user_rendering_restricted') + def test_render_inline_template_restricted(self): + """Test if we correctly detect static template.""" + res_ids = self.env['res.partner'].search([], limit=1).ids + with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'): + self.env['mail.render.mixin']._render_template_inline_template( + self.base_inline_template_bits[3], + 'res.partner', + res_ids + ) + + src = """

This is a static template

""" + result = self.env['mail.render.mixin']._render_template_inline_template( + src, + 'res.partner', + res_ids + )[res_ids[0]] + self.assertEqual(src, str(result)) + + @users('user_rendering_restricted') + def test_render_inline_template_restricted_static(self): + """Test that we render correctly static templates (without placeholders).""" + model = 'res.partner' + res_ids = self.env[model].search([], limit=1).ids + MailRenderMixin = self.env['mail.render.mixin'] + + result = MailRenderMixin._render_template_inline_template( + self.base_inline_template_bits[0], + model, + res_ids + )[res_ids[0]] + self.assertEqual(result, self.base_inline_template_bits[0]) + + @users('employee') + def test_render_inline_template_unrestricted(self): + """ Test if we correctly detect static template. """ + res_ids = self.env['res.partner'].search([], limit=1).ids + result = self.env['mail.render.mixin']._render_template_inline_template( + self.base_inline_template_bits[3], + 'res.partner', + res_ids + )[res_ids[0]] + self.assertIn('26', result, 'Template Editor should be able to render inline_template code') + + @users('user_rendering_restricted') + def test_render_template_qweb_restricted(self): + model = 'res.partner' + res_ids = self.env[model].search([], limit=1).ids + partner = self.env[model].browse(res_ids) + + src = """

This is a static template

""" + + result = self.env['mail.render.mixin']._render_template_qweb(src, model, res_ids)[ + partner.id] + self.assertEqual(src, str(result)) + + @users('user_rendering_restricted') + def test_security_function_call(self): + """Test the case when the template call a custom function. + + This function should not be called when the template is not rendered. + """ + model = 'res.partner' + res_ids = self.env[model].search([], limit=1).ids + partner = self.env[model].browse(res_ids) + MailRenderMixin = self.env['mail.render.mixin'] + + def cust_function(): + # Can not use "MagicMock" in a Jinja sand-boxed environment + # so create our own function + cust_function.call = True + return 'return value' + + cust_function.call = False + + src = """

This is a test

+

{{ cust_function() }}

""" + expected = """

This is a test

+

return value

""" + context = {'cust_function': cust_function} + + result = self.env['mail.render.mixin'].with_user(self.user_admin)._render_template_inline_template( + src, partner._name, partner.ids, + add_context=context + )[partner.id] + self.assertEqual(expected, result) + self.assertTrue(cust_function.call) + + with self.assertRaises(AccessError, msg='Simple user should not be able to render dynamic code'): + MailRenderMixin._render_template_inline_template(src, model, res_ids, add_context=context) + @users('user_rendering_restricted') def test_security_inline_template_restricted(self): """Test if we correctly detect condition block (which might contains code).""" @@ -384,7 +435,7 @@ class TestMailRender(common.MailCommon): self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[4], 'res.partner', res_ids) @users('employee') - def test_is_inline_template_condition_block_unrestricted(self): + def test_security_inline_template_unrestricted(self): """Test if we correctly detect condition block (which might contains code).""" res_ids = self.env['res.partner'].search([], limit=1).ids result = self.env['mail.render.mixin']._render_template_inline_template(self.base_inline_template_bits[4], 'res.partner', res_ids)[res_ids[0]] @@ -417,36 +468,3 @@ class TestMailRender(common.MailCommon): res_ids = self.env['res.partner'].search([], limit=1).ids result = self.env['mail.render.mixin']._render_template_qweb(self.base_qweb_bits[1], 'res.partner', res_ids)[res_ids[0]] self.assertNotIn('Code not executed', result, 'The condition block did not work') - - @users('user_rendering_restricted') - def test_template_rendering_static_inline_template(self): - model = 'res.partner' - res_ids = self.env[model].search([], limit=1).ids - partner = self.env[model].browse(res_ids) - - src = """

This is a static template

""" - - result = self.env['mail.render.mixin']._render_template_inline_template( - src, model, res_ids)[partner.id] - self.assertEqual(src, str(result)) - - @users('user_rendering_restricted') - def test_template_rendering_static_qweb(self): - model = 'res.partner' - res_ids = self.env[model].search([], limit=1).ids - partner = self.env[model].browse(res_ids) - - src = """

This is a static template

""" - - result = self.env['mail.render.mixin']._render_template_qweb(src, model, res_ids)[ - partner.id] - self.assertEqual(src, str(result)) - - @users('employee') - def test_prepend_preview_inline_template_to_qweb(self): - body = 'body' - preview = 'foo{{"false" if 1 > 2 else "true"}}bar' - result = self.env['mail.render.mixin']._prepend_preview(Markup(body), preview) - self.assertEqual(result, '''
- foobar -
body''') diff --git a/addons/mail/tests/test_mail_template.py b/addons/mail/tests/test_mail_template.py index 75ab522fad7..593f199c1d7 100644 --- a/addons/mail/tests/test_mail_template.py +++ b/addons/mail/tests/test_mail_template.py @@ -5,10 +5,11 @@ from markupsafe import Markup from odoo.addons.mail.tests.common import MailCommon from odoo.exceptions import AccessError, UserError from odoo.modules.module import get_module_resource -from odoo.tests import Form, users +from odoo.tests import Form, tagged, users from odoo.tools import convert_file +@tagged('mail_template') class TestMailTemplate(MailCommon): @classmethod @@ -147,6 +148,7 @@ class TestMailTemplate(MailCommon): self.assertFalse(server.active) +@tagged('mail_template') class TestMailTemplateReset(MailCommon): def _load(self, module, *args): diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 2414137ed80..3469f43d754 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -387,18 +387,18 @@ class MailComposer(models.TransientModel): for res_id in res_ids: # static wizard (mail.message) values mail_values = { - 'subject': self.subject, - 'body': self.body or '', - 'parent_id': self.parent_id and self.parent_id.id, - 'partner_ids': [partner.id for partner in self.partner_ids], 'attachment_ids': [attach.id for attach in self.attachment_ids], 'author_id': self.author_id.id, + 'body': self.body or '', 'email_from': self.email_from, - 'record_name': self.record_name, - 'reply_to_force_new': self.reply_to_force_new, 'mail_server_id': self.mail_server_id.id, 'mail_activity_type_id': self.mail_activity_type_id.id, 'message_type': 'email' if mass_mail_mode else self.message_type, + 'parent_id': self.parent_id.id, + 'partner_ids': self.partner_ids.ids, + 'record_name': self.record_name, + 'reply_to_force_new': self.reply_to_force_new, + 'subject': self.subject, } # mass mailing: rendering override wizard static values @@ -554,10 +554,12 @@ class MailComposer(models.TransientModel): template = self.env['mail.template'].browse(template_id) values = dict( (field, template[field]) - for field in ['subject', 'body_html', + for field in ('body_html', 'email_from', + 'mail_server_id', 'reply_to', - 'mail_server_id'] + 'subject', + ) if template[field] ) if template.attachment_ids: @@ -567,11 +569,16 @@ class MailComposer(models.TransientModel): elif template_id: values = self.generate_email_for_composer( template_id, [res_id], - ['subject', 'body_html', + ('attachment_ids', + 'body_html', + 'email_cc', 'email_from', - 'email_cc', 'email_to', 'partner_to', 'reply_to', - 'attachment_ids', 'mail_server_id' - ] + 'email_to', + 'mail_server_id', + 'partner_to', + 'reply_to', + 'subject', + ) )[res_id] # transform attachments into attachment_ids; not attached to the document because this will # be done further in the posting process, allowing to clean database if email not send @@ -593,19 +600,28 @@ class MailComposer(models.TransientModel): default_composition_mode=composition_mode, default_model=model, default_res_id=res_id - ).default_get(['composition_mode', 'model', 'res_id', 'parent_id', - 'subject', 'body', + ).default_get(['attachment_ids', + 'body', + 'composition_mode', 'email_from', - 'partner_ids', 'reply_to', - 'attachment_ids', 'mail_server_id' + 'mail_server_id', + 'model', + 'parent_id', + 'partner_ids', + 'reply_to', + 'res_id', + 'subject', ]) values = dict( (key, default_values[key]) - for key in ['subject', 'body', + for key in ('attachment_ids', + 'body', 'email_from', - 'partner_ids', 'reply_to', - 'attachment_ids', 'mail_server_id' - ] if key in default_values) + 'mail_server_id', + 'partner_ids', + 'reply_to', + 'subject', + ) if key in default_values) if values.get('body_html'): values['body'] = values.pop('body_html') @@ -653,10 +669,10 @@ class MailComposer(models.TransientModel): results = dict.fromkeys(res_ids, False) for res_id in res_ids: results[res_id] = { - 'subject': subjects[res_id], 'body': bodies[res_id], 'email_from': emails_from[res_id], 'reply_to': replies_to[res_id], + 'subject': subjects[res_id], } results[res_id].update(default_recipients.get(res_id, dict())) @@ -664,7 +680,13 @@ class MailComposer(models.TransientModel): if self.template_id: template_values = self.generate_email_for_composer( self.template_id.id, res_ids, - ['email_to', 'partner_to', 'email_cc', 'attachment_ids', 'mail_server_id']) + ('attachment_ids', + 'email_to', + 'email_cc', + 'mail_server_id', + 'partner_to', + ) + ) else: template_values = {} @@ -692,7 +714,7 @@ class MailComposer(models.TransientModel): multi_mode = False res_ids = [res_ids] - returned_fields = fields + ['partner_ids', 'attachments'] + returned_fields = list(fields) + ['partner_ids', 'attachments'] values = dict.fromkeys(res_ids, False) template_values = self.env['mail.template'].with_context(tpl_partners_only=True).browse(template_id).generate_email(res_ids, fields) diff --git a/addons/mail/wizard/mail_template_preview.py b/addons/mail/wizard/mail_template_preview.py index e82cb90e0fe..24ecbac2533 100644 --- a/addons/mail/wizard/mail_template_preview.py +++ b/addons/mail/wizard/mail_template_preview.py @@ -8,8 +8,15 @@ from odoo.exceptions import UserError class MailTemplatePreview(models.TransientModel): _name = 'mail.template.preview' _description = 'Email Template Preview' - _MAIL_TEMPLATE_FIELDS = ['subject', 'body_html', 'email_from', 'email_to', - 'email_cc', 'reply_to', 'scheduled_date', 'attachment_ids'] + _MAIL_TEMPLATE_FIELDS = ['attachment_ids', + 'body_html', + 'subject', + 'email_cc', + 'email_from', + 'email_to', + 'reply_to', + 'scheduled_date', + ] @api.model def _selection_target_model(self): @@ -70,7 +77,9 @@ class MailTemplatePreview(models.TransientModel): else: copy_depends_values['resource_ref'] = '%s,%s' % (self.resource_ref._name, self.resource_ref.id) mail_values = mail_template.with_context(template_preview_lang=self.lang).generate_email( - self.resource_ref.id, self._MAIL_TEMPLATE_FIELDS) + self.resource_ref.id, + self._MAIL_TEMPLATE_FIELDS + ) self._set_mail_attributes(values=mail_values) self.error_msg = False except UserError as user_error: diff --git a/addons/mass_mailing/models/mailing.py b/addons/mass_mailing/models/mailing.py index 0a68f7f3cb9..4fc7b40989a 100644 --- a/addons/mass_mailing/models/mailing.py +++ b/addons/mass_mailing/models/mailing.py @@ -1047,16 +1047,16 @@ class MassMailing(models.Model): 'author_id': author_id, 'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids], 'body': mailing._prepend_preview(mailing.body_html, mailing.preview), - 'subject': mailing.subject, - 'model': mailing.mailing_model_real, - 'email_from': mailing.email_from, - 'record_name': False, 'composition_mode': 'mass_mail', - 'mass_mailing_id': mailing.id, - 'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids], - 'reply_to_force_new': mailing.reply_to_mode == 'new', - 'template_id': None, + 'email_from': mailing.email_from, 'mail_server_id': mailing.mail_server_id.id, + 'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids], + 'mass_mailing_id': mailing.id, + 'model': mailing.mailing_model_real, + 'record_name': False, + 'reply_to_force_new': mailing.reply_to_mode == 'new', + 'subject': mailing.subject, + 'template_id': None, } if mailing.reply_to_mode == 'new': composer_values['reply_to'] = mailing.reply_to diff --git a/addons/mass_mailing/wizard/mail_compose_message.py b/addons/mass_mailing/wizard/mail_compose_message.py index dd971aed708..ff4c0148fb1 100644 --- a/addons/mass_mailing/wizard/mail_compose_message.py +++ b/addons/mass_mailing/wizard/mail_compose_message.py @@ -23,7 +23,6 @@ class MailComposeMessage(models.TransientModel): """ Override method that generated the mail content by creating the mailing.trace values in the o2m of mail_mail, when doing pure email mass mailing. """ - now = fields.Datetime.now() self.ensure_one() res = super(MailComposeMessage, self).get_mail_values(res_ids) # use only for allowed models in mass mailing @@ -32,37 +31,30 @@ class MailComposeMessage(models.TransientModel): self.model_is_thread: mass_mailing = self.mass_mailing_id if not mass_mailing: - mass_mailing = self.env['mailing.mailing'].create({ - 'campaign_id': self.campaign_id.id, - 'name': self.mass_mailing_name, - 'subject': self.subject, - 'state': 'done', - 'reply_to_mode': self.reply_to_mode, - 'reply_to': self.reply_to if self.reply_to_mode == 'new' else False, - 'sent_date': now, - 'body_html': self.body, - 'mailing_model_id': self.env['ir.model']._get(self.model).id, - 'mailing_domain': self.active_domain, - 'attachment_ids': [(6, 0, self.attachment_ids.ids)], - }) + mass_mailing = self.env['mailing.mailing'].create( + self._prepare_mailing_values() + ) self.mass_mailing_id = mass_mailing.id recipients_info = self._process_recipient_values(res) for res_id in res_ids: mail_values = res[res_id] if mail_values.get('body_html'): - body = self.env['ir.qweb']._render('mass_mailing.mass_mailing_mail_layout', - {'body': mail_values['body_html']}, - minimal_qcontext=True, raise_if_not_found=False) + body = self.env['ir.qweb']._render( + 'mass_mailing.mass_mailing_mail_layout', + {'body': mail_values['body_html']}, + minimal_qcontext=True, + raise_if_not_found=False + ) if body: mail_values['body_html'] = body trace_vals = { - 'model': self.model, - 'res_id': res_id, - 'mass_mailing_id': mass_mailing.id, # if mail_to is void, keep falsy values to allow searching / debugging traces 'email': recipients_info[res_id]['mail_to'][0] if recipients_info[res_id]['mail_to'] else '', + 'mass_mailing_id': mass_mailing.id, + 'model': self.model, + 'res_id': res_id, } # propagate failed states to trace when still-born if mail_values.get('state') == 'cancel': @@ -73,11 +65,11 @@ class MailComposeMessage(models.TransientModel): trace_vals['failure_type'] = mail_values['failure_type'] mail_values.update({ - 'mailing_id': mass_mailing.id, - 'mailing_trace_ids': [(0, 0, trace_vals)], + 'auto_delete': not mass_mailing.keep_archives, # email-mode: keep original message for routing 'is_notification': mass_mailing.reply_to_mode == 'update', - 'auto_delete': not mass_mailing.keep_archives, + 'mailing_id': mass_mailing.id, + 'mailing_trace_ids': [(0, 0, trace_vals)], }) return res @@ -92,3 +84,19 @@ class MailComposeMessage(models.TransientModel): if self.mass_mailing_id: opt_out_list += self.mass_mailing_id._get_opt_out_list() return opt_out_list + + def _prepare_mailing_values(self): + now = fields.Datetime.now() + return { + 'attachment_ids': [(6, 0, self.attachment_ids.ids)], + 'body_html': self.body, + 'campaign_id': self.campaign_id.id, + 'mailing_model_id': self.env['ir.model']._get(self.model).id, + 'mailing_domain': self.active_domain, + 'name': self.mass_mailing_name, + 'reply_to': self.reply_to if self.reply_to_mode == 'new' else False, + 'reply_to_mode': self.reply_to_mode, + 'sent_date': now, + 'state': 'done', + 'subject': self.subject, + } diff --git a/addons/sale/wizard/sale_order_cancel.py b/addons/sale/wizard/sale_order_cancel.py index 878500c220f..ab445a81758 100644 --- a/addons/sale/wizard/sale_order_cancel.py +++ b/addons/sale/wizard/sale_order_cancel.py @@ -84,12 +84,12 @@ class SaleOrderCancel(models.TransientModel): def action_send_mail_and_cancel(self): self.ensure_one() self.order_id.message_post( - subject=self.subject, body=self.body, message_type='comment', email_from=self.email_from, email_layout_xmlid='mail.mail_notification_light', partner_ids=self.recipient_ids.ids, + subject=self.subject, ) return self.action_cancel() diff --git a/addons/survey/wizard/survey_invite.py b/addons/survey/wizard/survey_invite.py index 09b5ce794b6..f0814ea8bf3 100644 --- a/addons/survey/wizard/survey_invite.py +++ b/addons/survey/wizard/survey_invite.py @@ -213,14 +213,14 @@ class SurveyInvite(models.TransientModel): body = self._render_field('body', answer.ids, post_process=True)[answer.id] # post the message mail_values = { - 'email_from': self.email_from, + 'attachment_ids': [(4, att.id) for att in self.attachment_ids], + 'auto_delete': True, 'author_id': self.author_id.id, + 'body_html': body, + 'email_from': self.email_from, 'model': None, 'res_id': None, 'subject': subject, - 'body_html': body, - 'attachment_ids': [(4, att.id) for att in self.attachment_ids], - 'auto_delete': True, } if answer.partner_id: mail_values['recipient_ids'] = [(4, answer.partner_id.id)] diff --git a/addons/test_mail/tests/test_ir_actions.py b/addons/test_mail/tests/test_ir_actions.py index cd6cb95f140..933886377f4 100644 --- a/addons/test_mail/tests/test_ir_actions.py +++ b/addons/test_mail/tests/test_ir_actions.py @@ -41,7 +41,7 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase): mail = self.env['mail.mail'].sudo().search([('subject', '=', 'About TestingPartner')]) self.assertEqual(len(mail), 1) self.assertTrue(mail.auto_delete) - self.assertEqual(mail.body, '

Hello TestingPartner

') + self.assertEqual(mail.body_html, '

Hello TestingPartner

') self.assertFalse(mail.is_notification) with self.mock_mail_gateway(mail_unlink_sent=True): mail.send() diff --git a/addons/test_mail/tests/test_mail_template.py b/addons/test_mail/tests/test_mail_template.py index 9be741b5fff..fb1bec1977e 100644 --- a/addons/test_mail/tests/test_mail_template.py +++ b/addons/test_mail/tests/test_mail_template.py @@ -64,9 +64,20 @@ class TestMailTemplateCommon(TestMailCommon, TestRecipients): cls.test_template.invalidate_recordset(['attachment_ids']) -@tagged('mail_template', 'multi_lang') +@tagged('mail_template') class TestMailTemplate(TestMailTemplateCommon): + def test_template_add_context_action(self): + self.test_template.create_action() + + # check template act_window has been updated + self.assertTrue(bool(self.test_template.ref_ir_act_window)) + + # check those records + action = self.test_template.ref_ir_act_window + self.assertEqual(action.name, 'Send Mail (%s)' % self.test_template.name) + self.assertEqual(action.binding_model_id.model, 'mail.test.lang') + @mute_logger('odoo.addons.mail.models.mail_mail') @users('employee') def test_template_schedule_email(self): @@ -93,6 +104,10 @@ class TestMailTemplate(TestMailTemplateCommon): self.assertFalse(mail.scheduled_date) self.assertEqual(mail.state, 'outgoing') + +@tagged('mail_template', 'multi_lang') +class TestMailTemplateLanguages(TestMailTemplateCommon): + @mute_logger('odoo.addons.mail.models.mail_mail') def test_template_send_email(self): mail_id = self.test_template.send_mail(self.test_record.id) @@ -134,54 +149,3 @@ class TestMailTemplate(TestMailTemplateCommon): self.assertEqual(mail.body_html, '

SpanishBody for %s

Spanish Layout para Spanish description' % self.test_record.name) self.assertEqual(mail.subject, 'SpanishSubject for %s' % self.test_record.name) - - def test_template_add_context_action(self): - self.test_template.create_action() - - # check template act_window has been updated - self.assertTrue(bool(self.test_template.ref_ir_act_window)) - - # check those records - action = self.test_template.ref_ir_act_window - self.assertEqual(action.name, 'Send Mail (%s)' % self.test_template.name) - self.assertEqual(action.binding_model_id.model, 'mail.test.lang') - - # def test_template_scheduled_date(self): - # from unittest.mock import patch - - # self.email_template_in_2_days = self.email_template.copy() - - # with patch('odoo.addons.mail.tests.test_mail_template.datetime', wraps=datetime) as mock_datetime: - # mock_datetime.now.return_value = datetime(2017, 11, 15, 11, 30, 28) - # mock_datetime.side_effect = lambda *args, **kw: datetime(*args, **kw) - - # self.email_template_in_2_days.write({ - # 'scheduled_date': "{{ (datetime.datetime.now() + relativedelta(days=2)).strftime('%s') }}" % DEFAULT_SERVER_DATETIME_FORMAT, - # }) - - # mail_now_id = self.email_template.send_mail(self.test_record.id) - # mail_in_2_days_id = self.email_template_in_2_days.send_mail(self.test_record.id) - - # mail_now = self.env['mail.mail'].browse(mail_now_id) - # mail_in_2_days = self.env['mail.mail'].browse(mail_in_2_days_id) - - # # mail preparation - # self.assertEqual(mail_now.exists() | mail_in_2_days.exists(), mail_now | mail_in_2_days) - # self.assertEqual(bool(mail_now.scheduled_date), False) - # self.assertEqual(mail_now.state, 'outgoing') - # self.assertEqual(mail_in_2_days.state, 'outgoing') - # scheduled_date = datetime.strptime(mail_in_2_days.scheduled_date, DEFAULT_SERVER_DATETIME_FORMAT) - # date_in_2_days = datetime.now() + timedelta(days = 2) - # self.assertEqual(scheduled_date, date_in_2_days) - # # self.assertEqual(scheduled_date.month, date_in_2_days.month) - # # self.assertEqual(scheduled_date.year, date_in_2_days.year) - - # # Launch the scheduler on the first mail, it should be reported in self.mails - # # and the mail_mail is now deleted - # self.env['mail.mail'].process_email_queue() - # self.assertEqual(mail_now.exists() | mail_in_2_days.exists(), mail_in_2_days) - - # # Launch the scheduler on the first mail, it's still in 'outgoing' state - # self.env['mail.mail'].process_email_queue(ids=[mail_in_2_days.id]) - # self.assertEqual(mail_in_2_days.state, 'outgoing') - # self.assertEqual(mail_now.exists() | mail_in_2_days.exists(), mail_in_2_days) diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py index 56ddcbf4a2b..7341c9fd44a 100644 --- a/addons/test_mail/tests/test_message_post.py +++ b/addons/test_mail/tests/test_message_post.py @@ -19,7 +19,6 @@ from odoo.exceptions import AccessError from odoo.tests import tagged from odoo.tools import mute_logger, formataddr from odoo.tests.common import users -from odoo.tools.translate import code_translations class TestMessagePostCommon(TestMailCommon, TestRecipients): @@ -1241,12 +1240,6 @@ class TestMessagePostLang(TestMailCommon, TestRecipients): @users('employee') @mute_logger('odoo.addons.mail.models.mail_mail') def test_layout_email_lang_context(self): - # hack into code translations: - code_translations.python_translations[('test_mail', 'es_ES')] = {'TestStuff': 'TestSpanishStuff'} - self.addCleanup(code_translations.python_translations.pop, ('test_mail', 'es_ES')) - code_translations.python_translations[('mail', 'es_ES')] = {'View %s': 'SpanishView %s'} - self.addCleanup(code_translations.python_translations.pop, ('mail', 'es_ES')) - test_records = self.test_records.with_user(self.env.user).with_context(lang='es_ES') test_records[1].message_subscribe(self.partner_2.ids) diff --git a/addons/test_mass_mailing/tests/test_mailing_test.py b/addons/test_mass_mailing/tests/test_mailing_test.py index d3bca1cf3c7..5e0d8ac149a 100644 --- a/addons/test_mass_mailing/tests/test_mailing_test.py +++ b/addons/test_mass_mailing/tests/test_mailing_test.py @@ -4,10 +4,11 @@ import lxml.html from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon from odoo.fields import Command -from odoo.tests.common import users +from odoo.tests.common import users, tagged from odoo.tools import mute_logger +@tagged('mailing_manage') class TestMailingTest(TestMassMailCommon): @users('user_marketing') diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index 26151b6f7b7..8654008772e 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -134,7 +134,16 @@ class ChannelUsersRelation(models.Model): record_email_values = dict() for template, records in template_to_records.items(): - record_email_values.update(template.generate_email(records.ids, ['subject', 'body_html', 'email_from', 'partner_to'])) + record_email_values.update( + template.generate_email( + records.ids, + ['body_html', + 'email_from', + 'partner_to', + 'subject', + ] + ) + ) mail_mail_values = [] for record in self: