diff --git a/addons/base_automation/models/base_automation.py b/addons/base_automation/models/base_automation.py index 6c314147cd8..717b220273c 100644 --- a/addons/base_automation/models/base_automation.py +++ b/addons/base_automation/models/base_automation.py @@ -104,7 +104,7 @@ class BaseAutomation(models.Model): raise exceptions.ValidationError( _('Form Modification based actions can only be used with code action type.') ) - if any(action.trigger == 'on_unlink' and action.state in ['email', 'followers', 'next_activity'] for action in self): + if any(action.trigger == 'on_unlink' and action.state in ['mail_post', 'followers', 'next_activity'] for action in self): raise exceptions.ValidationError( _('Email, followers or activities action types cannot be used when deleting records.') ) diff --git a/addons/mail/models/ir_actions_server.py b/addons/mail/models/ir_actions_server.py index 3d8be850312..87a7e24f651 100644 --- a/addons/mail/models/ir_actions_server.py +++ b/addons/mail/models/ir_actions_server.py @@ -8,27 +8,42 @@ from odoo.exceptions import ValidationError class ServerActions(models.Model): - """ Add email option in server actions. """ + """ Add mail.thread related options in server actions. """ _name = 'ir.actions.server' _description = 'Server Action' _inherit = ['ir.actions.server'] - state = fields.Selection(selection_add=[ - ('email', 'Send Email'), - ('followers', 'Add Followers'), - ('next_activity', 'Create Next Activity'), - ], ondelete={'email': 'cascade', 'followers': 'cascade', 'next_activity': 'cascade'}) + state = fields.Selection( + selection_add=[('mail_post', 'Send Email'), + ('followers', 'Add Followers'), + ('next_activity', 'Create Next Activity'), + ], + ondelete={'mail_post': 'cascade', + 'followers': 'cascade', + 'next_activity': 'cascade', + } + ) # Followers partner_ids = fields.Many2many( 'res.partner', string='Add Followers', compute='_compute_partner_ids', readonly=False, store=True) - # Template + # Message Post / Email template_id = fields.Many2one( 'mail.template', 'Email Template', domain="[('model_id', '=', model_id)]", compute='_compute_template_id', ondelete='set null', readonly=False, store=True, ) + # Message post + mail_post_autofollow = fields.Boolean( + 'Subscribe Recipients', compute='_compute_mail_post_autofollow', + readonly=False, store=True) + mail_post_method = fields.Selection( + selection=[('email', 'Email'), ('comment', 'Post as Message'), ('note', 'Post as Note')], + string='Send as', + compute='_compute_mail_post_method', + readonly=False, store=True, + help='Choose method for email sending:\nEMail: send directly emails\nPost as Message: post on document and notify followers\nPost as Note: log a note on document') # Next Activity activity_type_id = fields.Many2one( 'mail.activity.type', string='Activity', @@ -65,19 +80,37 @@ class ServerActions(models.Model): @api.depends('model_id', 'state') def _compute_template_id(self): to_reset = self.filtered( - lambda act: act.state != 'email' or \ + lambda act: act.state != 'mail_post' or \ (act.model_id != act.template_id.model_id) ) if to_reset: to_reset.template_id = False + @api.depends('state', 'mail_post_method') + def _compute_mail_post_autofollow(self): + to_reset = self.filtered(lambda act: act.state != 'mail_post' or act.mail_post_method == 'email') + if to_reset: + to_reset.mail_post_autofollow = False + other = self - to_reset + if other: + other.mail_post_autofollow = True + + @api.depends('state') + def _compute_mail_post_method(self): + to_reset = self.filtered(lambda act: act.state != 'mail_post') + if to_reset: + to_reset.mail_post_method = False + other = self - to_reset + if other: + other.mail_post_method = 'email' + @api.depends('state') def _compute_partner_ids(self): to_reset = self.filtered(lambda act: act.state != 'followers') if to_reset: to_reset.partner_ids = False - @api.depends('model_id') + @api.depends('model_id', 'state') def _compute_activity_type_id(self): to_reset = self.filtered( lambda act: act.state != 'next_activity' or \ @@ -106,7 +139,6 @@ class ServerActions(models.Model): if not activity.activity_user_field_name: activity.activity_user_field_name = 'user_id' - @api.constrains('activity_date_deadline_range') def _check_activity_date_deadline_range(self): if any(action.activity_date_deadline_range < 0 for action in self): @@ -155,17 +187,40 @@ class ServerActions(models.Model): return True return False - def _run_action_email(self, eval_context=None): + def _run_action_mail_post_multi(self, eval_context=None): # TDE CLEANME: when going to new api with server action, remove action - if not self.template_id or not self._context.get('active_id') or self._is_recompute(): + if not self.template_id or (not self._context.get('active_ids') and not self._context.get('active_id')) or self._is_recompute(): return False + res_ids = self._context.get('active_ids', [self._context.get('active_id')]) + # Clean context from default_type to avoid making attachment # with wrong values in subsequent operations cleaned_ctx = dict(self.env.context) cleaned_ctx.pop('default_type', None) cleaned_ctx.pop('default_parent_id', None) - self.template_id.with_context(cleaned_ctx).send_mail(self._context.get('active_id'), force_send=False, - raise_exception=False) + cleaned_ctx['mail_create_nosubscribe'] = True # do not subscribe random people to records + cleaned_ctx['mail_post_autofollow'] = self.mail_post_autofollow + + if self.mail_post_method in ('comment', 'note'): + records = self.env[self.model_name].with_context(cleaned_ctx).browse(res_ids) + if self.mail_post_method == 'comment': + subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment') + else: + subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note') + for record in records: + record.message_post_with_template( + self.template_id.id, + composition_mode='comment', + subtype_id=subtype_id, + ) + else: + template = self.template_id.with_context(cleaned_ctx) + for res_id in res_ids: + template.send_mail( + res_id, + force_send=False, + raise_exception=False + ) return False def _run_action_next_activity(self, eval_context=None): diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index a649235c51a..d53c33c8dfb 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -272,7 +272,7 @@ class MailTemplate(models.Model): Attachment = self.env['ir.attachment'] # TDE FIXME: should remove default_type from context # create a mail_mail based on values, without attachments - values = self.generate_email(res_id, ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'scheduled_date']) + values = self.generate_email(res_id, ['auto_delete', 'subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'scheduled_date']) 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())] values.update(email_values or {}) diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index b5d609d09ba..66dd23d8a80 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -819,6 +819,7 @@ class MailCase(MockEmail): mail_groups['failure'].append(partner) if ncheck_send: email_groups[ngroup].append(partner) + # canceled: currently nothing checked elif nstatus == 'canceled': pass else: diff --git a/addons/mail/views/ir_actions_server_views.xml b/addons/mail/views/ir_actions_server_views.xml index a3179a6d9a7..df2e7b082be 100644 --- a/addons/mail/views/ir_actions_server_views.xml +++ b/addons/mail/views/ir_actions_server_views.xml @@ -40,10 +40,14 @@ + attrs="{'invisible': [('state', '!=', 'followers')]}"/> + attrs="{'invisible': [('state', '!=', 'mail_post')], + 'required': [('state', '=', 'mail_post')]}"/> + + diff --git a/addons/test_base_automation/tests/test_flow.py b/addons/test_base_automation/tests/test_flow.py index 7e765af754a..7304a98a6ec 100644 --- a/addons/test_base_automation/tests/test_flow.py +++ b/addons/test_base_automation/tests/test_flow.py @@ -89,10 +89,11 @@ record['name'] = record.name + 'X'""", 'active': True, }, { 'name': 'Base Automation: test send an email', + 'mail_post_method': 'email', 'model_id': self.env.ref('test_base_automation.model_base_automation_lead_test').id, 'template_id': self.test_mail_template_automation.id, 'trigger_field_ids': [(4, self.env.ref('test_base_automation.field_base_automation_lead_test__deadline').id)], - 'state': 'email', + 'state': 'mail_post', 'code': """ record = model.browse(env.context['active_id']) record['name'] = record.name + 'X'""", diff --git a/addons/test_mail/tests/test_ir_actions.py b/addons/test_mail/tests/test_ir_actions.py index 5330b736150..cd6cb95f140 100644 --- a/addons/test_mail/tests/test_ir_actions.py +++ b/addons/test_mail/tests/test_ir_actions.py @@ -3,19 +3,53 @@ from odoo.addons.base.tests.test_ir_actions import TestServerActionsBase from odoo.addons.test_mail.tests.common import TestMailCommon +from odoo.tests import tagged +from odoo.tools import mute_logger +@tagged('ir_actions') class TestServerActionsEmail(TestMailCommon, TestServerActionsBase): + def setUp(self): + super(TestServerActionsEmail, self).setUp() + self.template = self._create_template( + 'res.partner', + {'email_from': '{{ object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted }}', + 'partner_to': '%s' % self.test_partner.id, + } + ) + + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink') def test_action_email(self): - email_template = self._create_template('res.partner', {'partner_to': '%s' % self.test_partner.id}) - self.action.write({'state': 'email', 'template_id': email_template.id}) - self.action.with_context(self.context).run() + # initial state + self.assertEqual(len(self.test_partner.message_ids), 1, + 'Contains Contact created message') + self.assertFalse(self.test_partner.message_partner_ids) + + # update action: send an email + self.action.write({ + 'mail_post_method': 'email', + 'state': 'mail_post', + 'template_id': self.template.id, + }) + self.assertFalse(self.action.mail_post_autofollow, 'Email action does not support autofollow') + + with self.mock_mail_app(): + self.action.with_context(self.context).run() + # check an email is waiting for sending mail = self.env['mail.mail'].sudo().search([('subject', '=', 'About TestingPartner')]) self.assertEqual(len(mail), 1) - # check email content + self.assertTrue(mail.auto_delete) self.assertEqual(mail.body, '

Hello TestingPartner

') + self.assertFalse(mail.is_notification) + with self.mock_mail_gateway(mail_unlink_sent=True): + mail.send() + + # no archive (message) + self.assertEqual(len(self.test_partner.message_ids), 1, + 'Contains Contact created message') + self.assertFalse(self.test_partner.message_partner_ids) def test_action_followers(self): self.test_partner.message_unsubscribe(self.test_partner.message_partner_ids.ids) @@ -27,6 +61,49 @@ class TestServerActionsEmail(TestMailCommon, TestServerActionsBase): self.action.with_context(self.context).run() self.assertEqual(self.test_partner.message_partner_ids, self.env.ref('base.partner_admin') | random_partner) + def test_action_message_post(self): + # initial state + self.assertEqual(len(self.test_partner.message_ids), 1, + 'Contains Contact created message') + self.assertFalse(self.test_partner.message_partner_ids) + + # test without autofollow and comment + self.action.write({ + 'mail_post_autofollow': False, + 'mail_post_method': 'comment', + 'state': 'mail_post', + 'template_id': self.template.id + }) + + with self.assertSinglePostNotifications( + [{'partner': self.test_partner, 'type': 'email', 'status': 'ready'}], + message_info={'content': 'Hello %s' % self.test_partner.name, + 'message_type': 'notification', + 'subtype': 'mail.mt_comment', + } + ): + self.action.with_context(self.context).run() + # NOTE: template using current user will have funny email_from + self.assertEqual(self.test_partner.message_ids[0].email_from, self.partner_root.email_formatted) + self.assertFalse(self.test_partner.message_partner_ids) + + # test with autofollow and note + self.action.write({ + 'mail_post_autofollow': True, + 'mail_post_method': 'note' + }) + with self.assertSinglePostNotifications( + [{'partner': self.test_partner, 'type': 'email', 'status': 'ready'}], + message_info={'content': 'Hello %s' % self.test_partner.name, + 'message_type': 'notification', + 'subtype': 'mail.mt_note', + } + ): + self.action.with_context(self.context).run() + self.assertEqual(len(self.test_partner.message_ids), 3, + '2 new messages produced') + self.assertEqual(self.test_partner.message_partner_ids, self.test_partner) + def test_action_next_activity(self): self.action.write({ 'state': 'next_activity', diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py index 3b97c978865..7d6c9de7e6b 100644 --- a/odoo/addons/base/models/ir_actions.py +++ b/odoo/addons/base/models/ir_actions.py @@ -415,12 +415,13 @@ class IrActionsServer(models.Model): default='object_write', required=True, copy=True, help="Type of server action. The following values are available:\n" "- 'Execute Python Code': a block of python code that will be executed\n" - "- 'Create': create a new record with new values\n" + "- 'Create a new Record': create a new record with new values\n" "- 'Update a Record': update the values of a record\n" "- 'Execute several actions': define an action that triggers several other server actions\n" - "- 'Send Email': automatically send an email (Discuss)\n" + "- 'Send Email': post a message, a note or send an email (Discuss)\n" "- 'Add Followers': add followers to a record (Discuss)\n" - "- 'Create Next Activity': create an activity (Discuss)") + "- 'Create Next Activity': create an activity (Discuss)\n" + "- 'Send SMS Text Message': send SMS, log them on documents (SMS)") # Generic sequence = fields.Integer(default=5, help="When dealing with multiple actions, the execution order is " diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py index 67215edaba2..8646a341278 100644 --- a/odoo/addons/base/tests/test_ir_actions.py +++ b/odoo/addons/base/tests/test_ir_actions.py @@ -23,9 +23,10 @@ class TestServerActionsBase(common.TransactionCase): 'address_format': 'SuperFormat', }) self.test_partner = self.env['res.partner'].create({ - 'name': 'TestingPartner', 'city': 'OrigCity', 'country_id': self.test_country.id, + 'email': 'test.partner@test.example.com', + 'name': 'TestingPartner', }) self.context = { 'active_model': 'res.partner',