[IMP] base_automation,base: support m2m operations in server actions

Before this commit, server actions of the 'Update Record' type could
only write on m2m fields entirely - meaning that there was no support
for commands like adding or removing records from the set without
writing it completely.

This commit introduces this possibility, along with some UI changes to
come from it regarding the display of such actions in automation rules.

Task-3450200

Part-of: odoo/odoo#138804
This commit is contained in:
Damien Bouvy
2023-10-26 13:41:07 +00:00
parent 72317d3017
commit 3871ae2c6c
4 changed files with 98 additions and 13 deletions
@@ -138,6 +138,8 @@
<field name="value" invisible="1" />
<field name="selection_value" invisible="1" />
<field name="value_field_to_show" invisible="1"/>
<field name="update_field_type" invisible="1"/>
<field name="update_m2m_operation" invisible="1"/>
<!-- Icon section -->
<i
@@ -158,16 +160,26 @@
/>
<field name="name" class="text-truncate" />
<t invisible="state != 'object_write'">
<span>to</span>
<code invisible="evaluation_type != 'equation'" t-esc="record.value.raw_value"/>
<span invisible="evaluation_type == 'equation'" >
<field name="selection_value" invisible="value_field_to_show != 'selection_value'" class="d-inline"/>
<span invisible="value_field_to_show == 'selection_value'">
<span invisible="value == False or evaluation_type == False">"</span>
<field name="value" class="d-inline"/>
<span invisible="value == False or evaluation_type == False">"</span>
</span>
</span>
<t invisible="not (update_field_type == 'many2many' and update_m2m_operation == 'clear') or evaluation_type != 'value'">
<span>by clearing it</span>
</t>
<t invisible="not (update_field_type == 'many2many' and update_m2m_operation == 'add') or evaluation_type != 'value'">
<span>by adding</span><field name="resource_ref"/>
</t>
<t invisible="not (update_field_type == 'many2many' and update_m2m_operation == 'remove') or evaluation_type != 'value'">
<span>by removing</span><field name="resource_ref"/>
</t>
<t invisible="not (update_field_type == 'many2many' and update_m2m_operation == 'set') or evaluation_type != 'value'">
<span>by setting it to</span><field name="resource_ref"/>
</t>
<t invisible="update_field_type == 'many2many' and evaluation_type == 'value'">
<span invisible="evaluation_type != 'value'">to</span>
<span invisible="evaluation_type != 'equation'">as</span>
<field name="resource_ref" invisible="not (value_field_to_show == 'resource_ref' and evaluation_type == 'value')" />
<field name="selection_value" invisible="not (value_field_to_show == 'selection_value' and evaluation_type == 'value')" class="d-inline"/>
<em invisible="not (value_field_to_show == 'value' and evaluation_type == 'value')" t-out="record.value.raw_value" class="d-inline"/>
<code invisible="not (evaluation_type == 'equation')" t-out="record.value.raw_value"/>
</t>
</t>
<button type="delete" name="delete" class="btn fa fa-trash fa-xl px-3 ms-auto" title="Delete Action" />
</div>
+18 -1
View File
@@ -527,6 +527,13 @@ class IrActionsServer(models.Model):
update_field_id = fields.Many2one('ir.model.fields', string='Field to Update', ondelete='cascade', compute='_compute_crud_relations', store=True, readonly=False)
update_path = fields.Char(string='Field to Update Path', help="Path to the field to update, e.g. 'partner_id.name'", default=_default_update_path)
update_related_model_id = fields.Many2one('ir.model', compute='_compute_crud_relations', store=True)
update_field_type = fields.Selection(related='update_field_id.ttype', readonly=True)
update_m2m_operation = fields.Selection([
('add', 'Adding'),
('remove', 'Removing'),
('set', 'Setting it to'),
('clear', 'Clearing it')
], string='Many2many Operations', default='add')
value = fields.Text(help="For Python expressions, this field may hold a Python expression "
"that can use the same values as for the code field on the server action,"
@@ -944,7 +951,7 @@ class IrActionsServer(models.Model):
@api.depends('evaluation_type', 'update_field_id')
def _compute_value_field_to_show(self): # check if value_field_to_show can be removed and use ttype in xml view instead
for action in self:
if action.update_field_id.ttype == 'many2one':
if action.update_field_id.ttype in ('many2one', 'many2many'):
action.value_field_to_show = 'resource_ref'
elif action.update_field_id.ttype == 'selection':
action.value_field_to_show = 'selection_value'
@@ -978,6 +985,16 @@ class IrActionsServer(models.Model):
expr = action.value
if action.evaluation_type == 'equation':
expr = safe_eval(action.value, eval_context)
elif action.update_field_id.ttype == 'many2many':
operation = action.update_m2m_operation
if operation == 'add':
expr = [Command.link(int(action.value))]
elif operation == 'remove':
expr = [Command.unlink(int(action.value))]
elif operation == 'set':
expr = [Command.set([int(action.value)])]
elif operation == 'clear':
expr = [Command.clear()]
elif action.update_field_id.ttype in ['many2one', 'integer']:
try:
expr = int(action.value)
+53
View File
@@ -226,6 +226,59 @@ ZeroDivisionError: division by zero""" % self.test_server_action.id
# Test: country updated
self.assertEqual(self.test_country.name_position, 'after')
def test_36_crud_write_m2m_ops(self):
""" Test that m2m operations work as expected """
categ_1 = self.env['res.partner.category'].create({'name': 'TestCateg1'})
categ_2 = self.env['res.partner.category'].create({'name': 'TestCateg2'})
# set partner category
self.action.write({
'state': 'object_write',
'update_path': 'category_id',
'update_m2m_operation': 'set',
'resource_ref': categ_1,
})
run_res = self.action.with_context(self.context).run()
self.assertFalse(run_res, 'ir_actions_server: update record action correctly finished should return False')
# Test: partner updated
self.assertIn(categ_1, self.test_partner.category_id, 'ir_actions_server: tag should have been set')
# add partner category
self.action.write({
'state': 'object_write',
'update_path': 'category_id',
'update_m2m_operation': 'add',
'resource_ref': categ_2,
})
run_res = self.action.with_context(self.context).run()
self.assertFalse(run_res, 'ir_actions_server: update record action correctly finished should return False')
# Test: partner updated
self.assertIn(categ_2, self.test_partner.category_id, 'ir_actions_server: new tag should have been added')
self.assertIn(categ_1, self.test_partner.category_id, 'ir_actions_server: old tag should still be there')
# remove partner category
self.action.write({
'state': 'object_write',
'update_path': 'category_id',
'update_m2m_operation': 'remove',
'resource_ref': categ_1,
})
run_res = self.action.with_context(self.context).run()
self.assertFalse(run_res, 'ir_actions_server: update record action correctly finished should return False')
# Test: partner updated
self.assertNotIn(categ_1, self.test_partner.category_id, 'ir_actions_server: tag should have been removed')
self.assertIn(categ_2, self.test_partner.category_id, 'ir_actions_server: tag should still be there')
# clear partner category
self.action.write({
'state': 'object_write',
'update_path': 'category_id',
'update_m2m_operation': 'clear',
})
run_res = self.action.with_context(self.context).run()
self.assertFalse(run_res, 'ir_actions_server: update record action correctly finished should return False')
# Test: partner updated
self.assertFalse(self.test_partner.category_id, 'ir_actions_server: tags should have been cleared')
def test_37_field_path_traversal(self):
""" Test the update_path field traversal - allowing records to be updated along relational links """
# update the country's name via the partner
+5 -2
View File
@@ -346,9 +346,12 @@
<field name="update_field_id" invisible="1"/>
<field name="value_field_to_show" invisible="1"/>
<field name="update_related_model_id" invisible="1"/>
<span invisible="evaluation_type != 'value'">to</span>
<field name="update_field_type" invisible="1"/>
<span invisible="evaluation_type != 'value' or not update_field_type == 'many2many'">by</span>
<field name="update_m2m_operation" class="oe_inline" invisible="evaluation_type != 'value' or not update_field_type == 'many2many'" required="update_field_type == 'many2many'"/>
<span invisible="evaluation_type != 'value' or update_field_type == 'many2many'">to</span>
<field name="value" class="oe_inline" placeholder="Set a value..." invisible="update_field_id == False or value_field_to_show != 'value' or evaluation_type != 'value'" string="Custom Value"/>
<field name="resource_ref" class="oe_inline" placeholder="Choose a value..." string="Custom Value" options="{'model_field': 'update_related_model_id', 'no_create': True, 'no_open': True}" invisible=" update_field_id == False or value_field_to_show != 'resource_ref' or evaluation_type == 'equation'"/>
<field name="resource_ref" class="oe_inline" placeholder="Choose a value..." string="Custom Value" options="{'model_field': 'update_related_model_id', 'no_create': True, 'no_open': True}" invisible=" update_field_id == False or value_field_to_show != 'resource_ref' or evaluation_type == 'equation' or update_m2m_operation == 'clear'"/>
<field name="selection_value" class="oe_inline" placeholder="Choose a value..." options="{'no_create': True, 'no_open': True}" invisible=" update_field_id == False or value_field_to_show != 'selection_value' or evaluation_type == 'equation'"/>
<span invisible="update_field_id != False or evaluation_type == 'equation'" class="text-muted">Set a value...</span>
<span invisible="evaluation_type != 'equation'">