diff --git a/addons/base_automation/__manifest__.py b/addons/base_automation/__manifest__.py index c28fc823021..81926b7c1d3 100644 --- a/addons/base_automation/__manifest__.py +++ b/addons/base_automation/__manifest__.py @@ -19,6 +19,7 @@ trigger an automatic reminder email. 'data': [ 'security/ir.model.access.csv', 'data/base_automation_data.xml', + 'views/ir_actions_server_views.xml', 'views/base_automation_views.xml', ], 'assets': { diff --git a/addons/base_automation/models/base_automation.py b/addons/base_automation/models/base_automation.py index d874a1dc0e8..571dd2b3834 100644 --- a/addons/base_automation/models/base_automation.py +++ b/addons/base_automation/models/base_automation.py @@ -8,7 +8,7 @@ from collections import defaultdict from dateutil.relativedelta import relativedelta -from odoo import _, api, Command, exceptions, fields, models +from odoo import _, api, exceptions, fields, models from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from odoo.tools import safe_eval @@ -177,6 +177,19 @@ class BaseAutomation(models.Model): CRITICAL_FIELDS = ['model_id', 'active', 'trigger', 'on_change_field_ids'] RANGE_FIELDS = ['trg_date_range', 'trg_date_range_type'] + @api.constrains('model_id', 'action_server_ids') + def _check_action_server_model(self): + for rule in self: + failing_actions = rule.action_server_ids.filtered( + lambda action: action.model_id != rule.model_id + ) + if failing_actions: + raise exceptions.ValidationError( + _('Target model of actions %(action_names)s are different from rule model.', + action_names=', '.join(failing_actions.mapped('name')) + ) + ) + @api.constrains('trigger', 'action_server_ids') def _check_trigger_state(self): for record in self: @@ -196,7 +209,14 @@ class BaseAutomation(models.Model): @api.depends('model_id') def _compute_action_server_ids(self): - self.action_server_ids = [Command.clear()] + """ When changing / setting model, remove actions that are not targeting + the same model anymore. """ + for rule in self.filtered('model_id'): + actions_to_remove = rule.action_server_ids.filtered( + lambda action: action.model_id != rule.model_id + ) + if actions_to_remove: + rule.action_server_ids = [(3, action.id) for action in actions_to_remove] @api.depends('trigger', 'trigger_field_ids') def _compute_trg_date_id(self): diff --git a/addons/base_automation/models/ir_actions_server.py b/addons/base_automation/models/ir_actions_server.py index 0cec609a320..782da84a3ed 100644 --- a/addons/base_automation/models/ir_actions_server.py +++ b/addons/base_automation/models/ir_actions_server.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import api, exceptions, fields, models, _ class ServerAction(models.Model): @@ -11,3 +11,23 @@ class ServerAction(models.Model): ('base_automation', 'Automation Rule') ], ondelete={'base_automation': 'cascade'}) base_automation_id = fields.Many2one('base.automation', string='Automation Rule', ondelete='cascade') + + @api.constrains('model_id', 'base_automation_id') + def _check_model_coherency_with_automation(self): + for action in self.filtered('base_automation_id'): + if action.model_id != action.base_automation_id.model_id: + raise exceptions.ValidationError( + _("Model of action %(action_name)s should match the one from automated rule %(rule_name)s.", + action_name=action.name, + rule_name=action.base_automation_id.name + ) + ) + + @api.depends('usage') + def _compute_available_model_ids(self): + """ Stricter model limit: based on automation rule """ + super()._compute_available_model_ids() + rule_based = self.filtered(lambda action: action.usage == 'base_automation') + for action in rule_based: + rule_model = action.base_automation_id.model_id + action.available_model_ids = rule_model.ids if rule_model in action.available_model_ids else [] diff --git a/addons/base_automation/views/base_automation_views.xml b/addons/base_automation/views/base_automation_views.xml index 94910894a44..469f4f62e1c 100644 --- a/addons/base_automation/views/base_automation_views.xml +++ b/addons/base_automation/views/base_automation_views.xml @@ -88,7 +88,7 @@ name="action_server_ids" widget="one2many" class="o_base_automation_actions_field" - context="{'default_model_id': model_id, 'default_state': False, 'automatic_action_name': True, 'form_view_ref': 'base_automation.view_server_action_form'}" + context="{'default_model_id': model_id, 'default_state': False, 'form_view_ref': 'base_automation.ir_actions_server_view_form_automation'}" > @@ -132,28 +132,6 @@ - - - base.automation.server.action.form - ir.actions.server - primary - - - - 1 - - - Type - - - 1 - - - 1 - - - - base.automation.tree diff --git a/addons/base_automation/views/ir_actions_server_views.xml b/addons/base_automation/views/ir_actions_server_views.xml new file mode 100644 index 00000000000..b7704c7b3cc --- /dev/null +++ b/addons/base_automation/views/ir_actions_server_views.xml @@ -0,0 +1,25 @@ + + + + + ir.actions.server.view.form.automation + ir.actions.server + primary + 32 + + + + 1 + + + Type + + + 1 + + + 1 + + + + diff --git a/addons/mail/models/ir_actions_server.py b/addons/mail/models/ir_actions_server.py index 61937bd3223..a60a7fe0803 100644 --- a/addons/mail/models/ir_actions_server.py +++ b/addons/mail/models/ir_actions_server.py @@ -78,22 +78,44 @@ class ServerActions(models.Model): 'User Field', compute='_compute_activity_info', readonly=False, store=True) - - @api.depends('state', 'template_id', 'partner_ids', 'activity_summary') + @api.depends('template_id', 'partner_ids', 'activity_summary') def _compute_name(self): - for action in self: - if not action.state or not self.env.context.get('automatic_action_name'): - continue + to_update = self.filtered( + lambda action: action.state in {'mail_post', 'followers', 'remove_followers', 'next_activity'} + ) + for action in to_update: if action.state == 'mail_post': - action.name = 'Send email: %s' % action.template_id.name + action.name = _( + 'Send email: %(template_name)s', + template_name=action.template_id.name + ) elif action.state == 'followers': - action.name = 'Add followers: %s' % ', '.join(action.partner_ids.mapped('name')) + action.name = _( + 'Add followers: %(partner_names)s', + partner_names=', '.join(action.partner_ids.mapped('name')) + ) elif action.state == 'remove_followers': - action.name = 'Remove followers: %s' % ', '.join(action.partner_ids.mapped('name')) + action.name = _( + 'Remove followers: %(partner_names)s', + partner_names=', '.join(action.partner_ids.mapped('name')) + ) elif action.state == 'next_activity': - action.name = 'Next activity: %s' % action.activity_summary - else: - super(ServerActions, action)._compute_name() + action.name = _( + 'Next activity: %(activity_name)s', + activity_name=action.activity_summary or action.activity_type_id.name + ) + super(ServerActions, self - to_update)._compute_name() + + @api.depends('state') + def _compute_available_model_ids(self): + mail_thread_based = self.filtered( + lambda action: action.state in {'mail_post', 'followers', 'remove_followers', 'next_activity'} + ) + if mail_thread_based: + mail_models = self.env['ir.model'].search([('is_mail_thread', '=', True), ('transient', '=', False)]) + for action in mail_thread_based: + action.available_model_ids = mail_models.ids + super(ServerActions, self - mail_thread_based)._compute_available_model_ids() @api.depends('model_id', 'state') def _compute_template_id(self): @@ -137,7 +159,7 @@ class ServerActions(models.Model): if to_reset: to_reset.activity_type_id = False - @api.depends('state') + @api.depends('state', 'activity_type_id') def _compute_activity_info(self): to_reset = self.filtered(lambda act: act.state != 'next_activity') if to_reset: @@ -149,26 +171,38 @@ class ServerActions(models.Model): to_reset.activity_user_id = False to_reset.activity_user_field_name = False to_default = self.filtered(lambda act: act.state == 'next_activity') - for activity in to_default: - if not activity.activity_date_deadline_range_type: - activity.activity_date_deadline_range_type = 'days' - if not activity.activity_user_type: - activity.activity_user_type = 'specific' - if not activity.activity_user_field_name: - activity.activity_user_field_name = 'user_id' + for action in to_default: + if not action.activity_summary: + action.activity_summary = action.activity_type_id.summary + if not action.activity_date_deadline_range_type: + action.activity_date_deadline_range_type = 'days' + if not action.activity_user_type: + action.activity_user_type = 'specific' + if not action.activity_user_field_name: + action.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): raise ValidationError(_("The 'Due Date In' value can't be negative.")) + @api.constrains('model_id', 'template_id') + def _check_mail_template_model(self): + for action in self.filtered(lambda action: action.state == 'mail_post'): + if action.template_id and action.template_id.model_id != action.model_id: + raise ValidationError( + _('Mail template model of %(action_name)s does not match action model.', + action_name=action.name + ) + ) + @api.constrains('state', 'model_id') - def _check_model_coherency(self): + def _check_mail_model_coherency(self): for action in self: - if action.state in ('followers', 'next_activity') and action.model_id.transient: + if action.state in {'mail_post', 'followers', 'remove_followers', 'next_activity'} and action.model_id.transient: raise ValidationError(_("This action cannot be done on transient models.")) - if action.state == 'followers' and not action.model_id.is_mail_thread: - raise ValidationError(_("Add Followers can only be done on a mail thread models")) + if action.state in {'mail_post', 'followers', 'remove_followers'} and not action.model_id.is_mail_thread: + raise ValidationError(_("This action can only be done on a mail thread models")) if action.state == 'next_activity' and not action.model_id.is_mail_activity: raise ValidationError(_("A next activity can only be planned on models that use activities.")) diff --git a/addons/sms/models/ir_actions_server.py b/addons/sms/models/ir_actions_server.py index a3463ddc483..c2a4a8e1162 100644 --- a/addons/sms/models/ir_actions_server.py +++ b/addons/sms/models/ir_actions_server.py @@ -26,15 +26,24 @@ class ServerActions(models.Model): compute='_compute_sms_method', readonly=False, store=True) - @api.depends('state', 'sms_template_id') + @api.depends('sms_template_id') def _compute_name(self): - for action in self: - if not action.state or not self.env.context.get('automatic_action_name'): - continue - if action.state == 'sms': - action.name = 'Send SMS: %s' % action.sms_template_id.name - else: - super(ServerActions, action)._compute_name() + to_update = self.filtered(lambda action: action.state == 'sms') + for action in to_update: + action.name = _( + 'Send SMS: %(template_name)s', + template_name=action.sms_template_id.name + ) + super(ServerActions, self - to_update)._compute_name() + + @api.depends('state') + def _compute_available_model_ids(self): + mail_thread_based = self.filtered(lambda action: action.state == 'sms') + if mail_thread_based: + mail_models = self.env['ir.model'].search([('is_mail_thread', '=', True), ('transient', '=', False)]) + for action in mail_thread_based: + action.available_model_ids = mail_models.ids + super(ServerActions, self - mail_thread_based)._compute_available_model_ids() @api.depends('model_id', 'state') def _compute_sms_template_id(self): @@ -54,12 +63,22 @@ class ServerActions(models.Model): if other: other.sms_method = 'sms' - def _check_model_coherency(self): - super()._check_model_coherency() + @api.constrains('state', 'model_id') + def _check_sms_model_coherency(self): for action in self: if action.state == 'sms' and (action.model_id.transient or not action.model_id.is_mail_thread): raise ValidationError(_("Sending SMS can only be done on a mail.thread or a transient model")) + @api.constrains('model_id', 'template_id') + def _check_sms_template_model(self): + for action in self.filtered(lambda action: action.state == 'sms'): + if action.sms_template_id and action.sms_template_id.model_id != action.model_id: + raise ValidationError( + _('SMS template model of %(action_name)s does not match action model.', + action_name=action.name + ) + ) + def _run_action_sms_multi(self, eval_context=None): # TDE CLEANME: when going to new api with server action, remove action if not self.sms_template_id or self._is_recompute(): diff --git a/addons/test_base_automation/models/test_base_automation.py b/addons/test_base_automation/models/test_base_automation.py index 9f2419c5a06..ea7ebe7c12a 100644 --- a/addons/test_base_automation/models/test_base_automation.py +++ b/addons/test_base_automation/models/test_base_automation.py @@ -42,6 +42,12 @@ class LeadTest(models.Model): return result +class LeadThreadTest(models.Model): + _name = "base.automation.lead.thread.test" + _description = "Automated Rule Test With Thread" + _inherit = ['base.automation.lead.test', 'mail.thread'] + + class LineTest(models.Model): _name = "base.automation.line.test" _description = "Automated Rule Line Test" diff --git a/addons/test_base_automation/security/ir.model.access.csv b/addons/test_base_automation/security/ir.model.access.csv index f3aa6170171..60ff30f2232 100644 --- a/addons/test_base_automation/security/ir.model.access.csv +++ b/addons/test_base_automation/security/ir.model.access.csv @@ -1,5 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_base_automation_lead_test,access_base_automation_lead_test,model_base_automation_lead_test,base.group_system,1,1,1,1 +access_base_automation_lead_thread_test,access_base_automation_lead_thread_test,model_base_automation_lead_thread_test,base.group_system,1,1,1,1 access_base_automation_line_test,access_base_automation_line_test,model_base_automation_line_test,base.group_system,1,1,1,1 access_base_automation_link_test,access_base_automation_link_test,model_base_automation_link_test,base.group_user,1,1,1,1 access_base_automation_linked_test,access_base_automation_linked_test,model_base_automation_linked_test,base.group_user,1,1,1,1 diff --git a/addons/test_base_automation/static/tests/tour/base_automation_tour.js b/addons/test_base_automation/static/tests/tour/base_automation_tour.js index c771f50e748..d93a6589af1 100644 --- a/addons/test_base_automation/static/tests/tour/base_automation_tour.js +++ b/addons/test_base_automation/static/tests/tour/base_automation_tour.js @@ -431,28 +431,6 @@ registry.category("web_tour.tours").add("test_form_view_resequence_actions", { ], }); -registry.category("web_tour.tours").add("test_form_view_debug", { - test: true, - steps: () => [ - { - trigger: - ".o_form_renderer .o_field_widget[name='action_server_ids'] .o_kanban_view .o_cp_buttons button", - }, - { - trigger: ".modal-content .o_form_renderer", - run() { - const allFields = this.$anchor[0].querySelectorAll(".o_field_widget[name]"); - assertEqual( - Array.from(allFields) - .map((el) => el.getAttribute("name")) - .includes("model_id"), - true - ); - }, - }, - ], -}); - let waitOrmCalls; registry.category("web_tour.tours").add("test_form_view_model_id", { test: true, diff --git a/addons/test_base_automation/tests/test_flow.py b/addons/test_base_automation/tests/test_flow.py index 057c77dd793..48d3c1370b9 100644 --- a/addons/test_base_automation/tests/test_flow.py +++ b/addons/test_base_automation/tests/test_flow.py @@ -49,7 +49,7 @@ class BaseAutomationTest(TransactionCaseWithUserDemo): self.test_mail_template_automation = self.env['mail.template'].create( { 'name': 'Template Automation', - 'model_id': self.lead_model.id, + 'model_id': self.env['ir.model']._get_id("base.automation.lead.thread.test"), 'body_html': """<div>Email automation</div>""", } ) @@ -365,7 +365,7 @@ else: deadline_field = self.env.ref("test_base_automation.field_base_automation_lead_test__deadline") create_automation( self, - model_id=self.lead_model.id, + model_id=self.env['ir.model']._get_id('base.automation.lead.thread.test'), trigger='on_create_or_write', trigger_field_ids=[Command.link(deadline_field.id)], filter_pre_domain="[('deadline', '=', False)]", @@ -386,7 +386,11 @@ else: patcher = patch('odoo.addons.mail.models.mail_template.MailTemplate.send_mail', _patched_send_mail) self.startPatcher(patcher) - lead = self.create_lead() + lead = self.env['base.automation.lead.thread.test'].create({ + 'name': "Lead Test", + 'user_id': self.user_root.id, + }) + self.addCleanup(lead.unlink) self.assertEqual(lead.priority, False) self.assertEqual(lead.deadline, False) self.assertEqual(send_mail_count, 0) diff --git a/addons/test_base_automation/tests/test_tour.py b/addons/test_base_automation/tests/test_tour.py index e47d77da1b2..ed3e0028e31 100644 --- a/addons/test_base_automation/tests/test_tour.py +++ b/addons/test_base_automation/tests/test_tour.py @@ -265,23 +265,6 @@ class BaseAutomationTestUi(HttpCase): ["Set Active To False 2", "Set Active To False 0", "Set Active To False 1"], ) - def test_form_view_debug(self): - model = self.env.ref("base.model_res_partner") - automation = self.env["base.automation"].create( - { - "name": "Test", - "trigger": "on_create_or_write", - "model_id": model.id, - } - ) - self.start_tour( - ( - f"/web?debug=1#{_urlencode_kwargs(action=self.env.ref('base_automation.base_automation_act').id, id=automation.id, view_type='form')}" - ), - "test_form_view_debug", - login="admin", - ) - def test_form_view_model_id(self): self.start_tour( ( diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py index bbe6cec211a..1f67f6693c2 100644 --- a/odoo/addons/base/models/ir_actions.py +++ b/odoo/addons/base/models/ir_actions.py @@ -485,6 +485,7 @@ class IrActionsServer(models.Model): "based on the sequence. Low number means high priority.") model_id = fields.Many2one('ir.model', string='Model', required=True, ondelete='cascade', index=True, help="Model on which the server action runs.") + available_model_ids = fields.Many2many('ir.model', string='Available Models', compute='_compute_available_model_ids', store=False) model_name = fields.Char(related='model_id.model', string='Model Name', readonly=True, store=True) # Python code code = fields.Text(string='Python Code', groups='base.group_system', @@ -533,16 +534,27 @@ class IrActionsServer(models.Model): @api.depends('state', 'update_field_id', 'crud_model_id', 'value') def _compute_name(self): - for action in self: - if not action.state or not self.env.context.get('automatic_action_name'): - continue + for action in self.filtered('state'): if action.state == 'object_write': - action.name = _("Update %s", action.update_field_id.field_description) + action.name = _( + "Update %(field_name)s", + field_name=action.update_field_id.field_description + ) elif action.state == 'object_create': - action.name = _("Create %s with name %s", action.crud_model_id.name, action.value) + action.name = _( + "Create %(model_name)s with name %(value)s", + model_name=action.crud_model_id.name, + value=action.value + ) else: - state_name = dict(action._fields['state']._description_selection(self.env))[action.state] - action.name = state_name + action.name = dict(action._fields['state']._description_selection(self.env))[action.state] + + @api.depends('state') + def _compute_available_model_ids(self): + allowed_models = self.env['ir.model'].search( + [('model', 'in', list(self.env['ir.model.access']._get_allowed_models()))] + ) + self.available_model_ids = allowed_models.ids @api.onchange('model_id') def _compute_crud_model_id(self): diff --git a/odoo/addons/base/views/ir_actions_views.xml b/odoo/addons/base/views/ir_actions_views.xml index eca3223fda1..fa9fce9728e 100644 --- a/odoo/addons/base/views/ir_actions_views.xml +++ b/odoo/addons/base/views/ir_actions_views.xml @@ -332,9 +332,11 @@ - - - + + +