[IMP] base_automation: implement mail triggers
This commit implements mail triggers on base automation as follows: - on_message_sent/on_message_received are new triggers on base_automation - only reacts on message_post, an heuristic has been chosen to determine whether a message has been sent to an external partner, or received from one. task-id-3450200 Part-of: odoo/odoo#135932
This commit is contained in:
@@ -54,6 +54,8 @@ WRITE_TRIGGERS = [
|
||||
'on_user_set',
|
||||
]
|
||||
|
||||
MAIL_TRIGGERS = ("on_message_received", "on_message_sent")
|
||||
|
||||
CREATE_WRITE_SET = set(CREATE_TRIGGERS + WRITE_TRIGGERS)
|
||||
|
||||
TIME_TRIGGERS = [
|
||||
@@ -71,7 +73,8 @@ class BaseAutomation(models.Model):
|
||||
model_id = fields.Many2one(
|
||||
"ir.model", string="Model", required=True, ondelete="cascade", help="Model on which the automation rule runs."
|
||||
)
|
||||
model_name = fields.Char(related="model_id.model", string="Model Name", readonly=True)
|
||||
model_name = fields.Char(related="model_id.model", string="Model Name", readonly=True, inverse="_inverse_model_name")
|
||||
model_is_mail_thread = fields.Boolean(related="model_id.is_mail_thread")
|
||||
action_server_ids = fields.One2many("ir.actions.server", "base_automation_id",
|
||||
context={'default_usage': 'base_automation'},
|
||||
string="Actions",
|
||||
@@ -80,6 +83,13 @@ class BaseAutomation(models.Model):
|
||||
readonly=False,
|
||||
)
|
||||
active = fields.Boolean(default=True, help="When unchecked, the rule is hidden and will not be executed.")
|
||||
|
||||
@api.constrains("trigger", "model_id")
|
||||
def _check_trigger(self):
|
||||
for automation in self:
|
||||
if automation.trigger in MAIL_TRIGGERS and not automation.model_id.is_mail_thread:
|
||||
raise exceptions.ValidationError(_("Mail event can not be configured on model %s. Only models with discussion feature can be used.", automation.model_id.name))
|
||||
|
||||
trigger = fields.Selection(
|
||||
[
|
||||
('on_stage_set', "Stage is set to"),
|
||||
@@ -99,6 +109,9 @@ class BaseAutomation(models.Model):
|
||||
('on_time', "Based on date field"),
|
||||
('on_time_created', "After creation"),
|
||||
('on_time_updated', "After last update"),
|
||||
|
||||
("on_message_received", "A message was received from an external user"),
|
||||
("on_message_sent", "A message was sent to an external user"),
|
||||
], string='Trigger',
|
||||
compute='_compute_trigger_and_trigger_field_ids', readonly=False, store=True, required=True)
|
||||
trg_selection_field_id = fields.Many2one(
|
||||
@@ -338,6 +351,8 @@ class BaseAutomation(models.Model):
|
||||
else:
|
||||
automation.trigger_field_ids = False
|
||||
continue
|
||||
if automation.model_id.is_mail_thread and automation.trigger in MAIL_TRIGGERS:
|
||||
continue
|
||||
|
||||
automation.trigger_field_ids = self.env['ir.model.fields'].search(domain, limit=1)
|
||||
automation.trigger = False if not automation.trigger_field_ids else automation.trigger
|
||||
@@ -490,7 +505,7 @@ class BaseAutomation(models.Model):
|
||||
def _process(self, records, domain_post=None):
|
||||
""" Process automation ``self`` on the ``records`` that have not been done yet. """
|
||||
# filter out the records on which self has already been done
|
||||
automation_done = self._context['__action_done']
|
||||
automation_done = self._context.get('__action_done', {})
|
||||
records_done = automation_done.get(self, records.browse())
|
||||
records -= records_done
|
||||
if not records:
|
||||
@@ -722,9 +737,32 @@ class BaseAutomation(models.Model):
|
||||
for field in automation_rule.on_change_field_ids:
|
||||
Model._onchange_methods[field.name].append(method)
|
||||
|
||||
if automation_rule.model_id.is_mail_thread and automation_rule.trigger in MAIL_TRIGGERS:
|
||||
def _message_post(self, *args, **kwargs):
|
||||
message = _message_post.origin(self, *args, **kwargs)
|
||||
# Don't execute automations for a message emitted during
|
||||
# the run of automations for a real message
|
||||
# Don't execute if we know already that a message is only internal
|
||||
message_sudo = message.sudo().with_context(active_test=False)
|
||||
if "__action_done" in self.env.context or message_sudo.is_internal or message_sudo.subtype_id.internal:
|
||||
return message
|
||||
if message_sudo.message_type in ('notification', 'auto_comment', 'user_notification'):
|
||||
return message
|
||||
|
||||
# always execute actions when the author is a customer
|
||||
mail_trigger = "on_message_received" if message_sudo.author_id.partner_share else "on_message_sent"
|
||||
automations = self.env['base.automation']._get_actions(self, [mail_trigger])
|
||||
for automation in automations.with_context(old_values=None):
|
||||
records = automation._filter_pre(self)
|
||||
automation._process(records)
|
||||
|
||||
return message
|
||||
|
||||
patch(Model, "message_post", _message_post)
|
||||
|
||||
def _unregister_hook(self):
|
||||
""" Remove the patches installed by _register_hook() """
|
||||
NAMES = ['create', 'write', '_compute_field_value', 'unlink', '_onchange_methods']
|
||||
NAMES = ['create', 'write', '_compute_field_value', 'unlink', '_onchange_methods', "message_post"]
|
||||
for Model in self.env.registry.values():
|
||||
for name in NAMES:
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { useState } from "@odoo/owl";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { _lt, _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useRecordObserver } from "@web/model/relational_model/utils";
|
||||
import { selectionField, SelectionField } from "@web/views/fields/selection/selection_field";
|
||||
@@ -31,25 +31,34 @@ const OPT_GROUPS = [
|
||||
triggers: ["on_unlink", "on_change"],
|
||||
},
|
||||
{
|
||||
group: { sequence: 50, key: "deprecated", name: _lt("Deprecated (do not use)") },
|
||||
group: { sequence: 20, key: "mail", name: _t("Email Events") },
|
||||
triggers: ["on_message_sent", "on_message_received"],
|
||||
},
|
||||
{
|
||||
group: { sequence: 60, key: "deprecated", name: _lt("Deprecated (do not use)") },
|
||||
triggers: ["on_create", "on_write"],
|
||||
},
|
||||
];
|
||||
|
||||
function computeDerivedOptions(options, fields, recordValue) {
|
||||
function computeDerivedOptions(options, fields, currentSelection, { excludeGroups = [] } = {}) {
|
||||
// filter options to display, derived from the current value and the model fields
|
||||
const derivedOptions = [];
|
||||
for (const [value, label] of options) {
|
||||
const { group, triggers } = OPT_GROUPS.find((g) => g.triggers.includes(value));
|
||||
if (group.key === "deprecated" && !triggers.includes(recordValue)) {
|
||||
if (
|
||||
(group.key === "deprecated" && !triggers.includes(currentSelection)) ||
|
||||
excludeGroups.includes(group.key)
|
||||
) {
|
||||
// skip deprecated triggers if the current value is not deprecated
|
||||
continue;
|
||||
}
|
||||
const filterFn = TRIGGER_FILTERS[value];
|
||||
const triggerFields = fields.filter(filterFn);
|
||||
if (triggerFields.length === 0) {
|
||||
// skip triggers that don't have any corresponding field
|
||||
continue;
|
||||
if (filterFn) {
|
||||
const triggerFields = fields.filter(filterFn);
|
||||
if (triggerFields.length === 0) {
|
||||
// skip triggers that don't have any corresponding field
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const option = { group, value, label };
|
||||
derivedOptions.push(option);
|
||||
@@ -82,7 +91,8 @@ export class TriggerSelectionField extends SelectionField {
|
||||
const derivedOptions = computeDerivedOptions(
|
||||
fields[this.props.name].selection,
|
||||
relatedModelFields,
|
||||
data[this.props.name]
|
||||
data[this.props.name],
|
||||
{ excludeGroups: data.model_is_mail_thread ? [] : ["mail"] }
|
||||
);
|
||||
|
||||
// then group and sort them
|
||||
@@ -105,5 +115,6 @@ export class TriggerSelectionField extends SelectionField {
|
||||
export const triggerSelectionField = {
|
||||
...selectionField,
|
||||
component: TriggerSelectionField,
|
||||
fieldDependencies: [{ name: "model_is_mail_thread", type: "boolean" }],
|
||||
};
|
||||
registry.category("fields").add("base_automation_trigger_selection", triggerSelectionField);
|
||||
|
||||
@@ -109,3 +109,8 @@ class Stage(models.Model):
|
||||
class Tag(models.Model):
|
||||
_name = _description = 'test_base_automation.tag'
|
||||
name = fields.Char()
|
||||
|
||||
class LeadThread(models.Model):
|
||||
_inherit = ["base.automation.lead.test", "mail.thread"]
|
||||
_name = "base.automation.lead.thread.test"
|
||||
_description = "Threaded Lead Test"
|
||||
|
||||
@@ -550,3 +550,56 @@ registry.category("web_tour.tours").add("test_form_view_custom_reference_field",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
registry.category("web_tour.tours").add("test_form_view_mail_triggers", {
|
||||
test: true,
|
||||
steps: () => [
|
||||
{
|
||||
trigger: ".o_field_widget[name='model_id'] input",
|
||||
run: "text base.automation.lead.test",
|
||||
},
|
||||
{
|
||||
trigger:
|
||||
".o_field_widget[name='model_id'] .dropdown-menu li a:contains(Automated Rule Test)",
|
||||
},
|
||||
{
|
||||
trigger: ".o_field_widget[name='trigger'] select",
|
||||
run() {
|
||||
assertEqual(Array.from(this.$anchor[0].querySelectorAll("optgroup")).map(el => el.label).join(", "), "Values Updated, Timing Conditions, Custom")
|
||||
}
|
||||
},
|
||||
{
|
||||
trigger: ".o_field_widget[name='model_id'] input",
|
||||
run: "text base.automation.lead.thread.test",
|
||||
},
|
||||
{
|
||||
trigger:
|
||||
".o_field_widget[name='model_id'] .dropdown-menu li a:contains(Threaded Lead Test)",
|
||||
run(helpers) {
|
||||
waitOrmCalls = observeOrmCalls();
|
||||
helpers.click(this.$anchor);
|
||||
return nextTick();
|
||||
},
|
||||
},
|
||||
{
|
||||
trigger: "body",
|
||||
async run() {
|
||||
await waitOrmCalls();
|
||||
await nextTick();
|
||||
},
|
||||
},
|
||||
{
|
||||
trigger: ".o_field_widget[name='trigger']",
|
||||
run() {
|
||||
assertEqual(Array.from(this.$anchor[0].querySelectorAll("select optgroup")).map(el => el.label).join(", "), "Values Updated, Email Events, Timing Conditions, Custom")
|
||||
}
|
||||
},
|
||||
{
|
||||
trigger: "button.o_form_button_cancel",
|
||||
},
|
||||
{
|
||||
trigger: "body:not(:has(button.o_form_button_cancel)",
|
||||
run() {}
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
from unittest.mock import patch
|
||||
import sys
|
||||
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
|
||||
from odoo.tests import common, tagged
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.exceptions import AccessError, ValidationError
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@@ -959,3 +960,51 @@ class TestCompute(common.TransactionCase):
|
||||
task.unlink()
|
||||
finally:
|
||||
sys.setrecursionlimit(limit)
|
||||
|
||||
def test_mail_triggers(self):
|
||||
lead_model = self.env["ir.model"]._get("base.automation.lead.test")
|
||||
with self.assertRaises(ValidationError):
|
||||
create_automation(self, trigger="on_message_sent", model_id=lead_model.id)
|
||||
|
||||
lead_thread_model = self.env["ir.model"]._get("base.automation.lead.thread.test")
|
||||
automation = create_automation(self, trigger="on_message_sent", model_id=lead_thread_model.id, _actions={
|
||||
"state": "object_write",
|
||||
"update_field_id": self.env["ir.model.fields"]._get("base.automation.lead.thread.test", "active").id,
|
||||
"value": False
|
||||
})
|
||||
|
||||
ext_partner = self.env["res.partner"].create({"name": "ext", "email": "email@server.com"})
|
||||
internal_partner = self.env["res.users"].browse(2).partner_id
|
||||
|
||||
obj = self.env["base.automation.lead.thread.test"].create({"name": "test"})
|
||||
obj.message_subscribe([ext_partner.id, internal_partner.id])
|
||||
|
||||
obj.message_post(author_id=internal_partner.id, message_type="comment", subtype_xmlid="mail.mt_comment")
|
||||
self.assertFalse(obj.active)
|
||||
|
||||
obj.active = True
|
||||
obj.message_post(author_id=internal_partner.id, subtype_xmlid="mail.mt_comment")
|
||||
self.assertTrue(obj.active)
|
||||
|
||||
obj.message_post(author_id=ext_partner.id, message_type="comment")
|
||||
self.assertTrue(obj.active)
|
||||
|
||||
obj.message_post(author_id=internal_partner.id, message_type="comment")
|
||||
self.assertTrue(obj.active)
|
||||
obj.message_post(author_id=internal_partner.id, subtype_xmlid="mail.mt_comment", message_type="comment")
|
||||
self.assertFalse(obj.active)
|
||||
|
||||
automation.trigger = "on_message_received"
|
||||
obj.active = True
|
||||
obj.message_post(author_id=internal_partner.id, subtype_xmlid="mail.mt_comment", message_type="comment")
|
||||
self.assertTrue(obj.active)
|
||||
|
||||
obj.message_post(author_id=ext_partner.id, message_type="comment")
|
||||
self.assertTrue(obj.active)
|
||||
|
||||
obj.message_post(author_id=ext_partner.id, subtype_xmlid="mail.mt_comment", message_type="comment")
|
||||
self.assertFalse(obj.active)
|
||||
|
||||
obj.active = True
|
||||
obj.message_post(author_id=ext_partner.id, subtype_xmlid="mail.mt_comment")
|
||||
self.assertTrue(obj.active)
|
||||
|
||||
@@ -284,3 +284,12 @@ class BaseAutomationTestUi(HttpCase):
|
||||
"test_form_view_custom_reference_field",
|
||||
login="admin",
|
||||
)
|
||||
|
||||
def test_form_view_mail_triggers(self):
|
||||
self.start_tour(
|
||||
(
|
||||
f"/web?debug=0#{_urlencode_kwargs(action=self.env.ref('base_automation.base_automation_act').id, view_type='form')}"
|
||||
),
|
||||
"test_form_view_mail_triggers",
|
||||
login="admin",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user