[IMP] mail: add a post server action
Currently only an email action based on template exists in server actions when having mail app installed. It basically sends an email based on a mail template. However being able to post a message on record is also useful. Instead of sending emails it post on a document as a comment or as a note, like what users can do using the chatter. Notification flow for those cases is the classic from post: followers, specified partners, Inbox/Email, ... Task-2613245 (Server actions mail update / cleaning) Closes #45640 Part-of: odoo/odoo#75906
This commit is contained in:
@@ -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.')
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 {})
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -40,10 +40,14 @@
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='link_field_id']" position="after">
|
||||
<field name="partner_ids" widget="many2many_tags"
|
||||
attrs="{'invisible': [('state', '!=', 'followers')]}"/>
|
||||
attrs="{'invisible': [('state', '!=', 'followers')]}"/>
|
||||
<field name="template_id"
|
||||
attrs="{'invisible': [('state', '!=', 'email')],
|
||||
'required': [('state', '=', 'email')]}"/>
|
||||
attrs="{'invisible': [('state', '!=', 'mail_post')],
|
||||
'required': [('state', '=', 'mail_post')]}"/>
|
||||
<field name="mail_post_method"
|
||||
attrs="{'invisible': [('state', '!=', 'mail_post')],
|
||||
'required': [('state', '=', 'mail_post')]}"/>
|
||||
<field name="mail_post_autofollow" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -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'""",
|
||||
|
||||
@@ -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, '<p>Hello TestingPartner</p>')
|
||||
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',
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user