Skip to content

Commit 7fc1c85

Browse files
pauloamedKangOl
andcommitted
[IMP] util/records: enforce cascade removal for actions
The implementation of the python inheritance mechanism between the base class `ir.actions.actions` and its child classes (eg. `ir.actions.act_window`) does not allow the creation of foreign keys when `ir.actions.actions` is a M2O field of another model when it is being referenced in favour of polymorphism; what leads to the not execution of some constraints, one of them being the `ondelete='cascade'` constraint, which is set in PSQL level. That said, when a `ir.actions.actions` record is deleted, if it is being referenced as a M2O field by another model (eg. `ir.filters`), records from this second model won't be affected, what leads to undesired behaviour: a MissingError in the UI, indicating that the action was deleted. Such behaviour of not creating foreign keys and thus constraints is specific to `ir.actions.actions`. This commit remedies this specific case, removing records with a M2O field to `ir.actions.actions` if `ondelete=cascade`, or setting this field to NULL if `ondelete=set null`, when the action referenced in such field is removed using `upgrade-util`. Co-authored-by: Christophe Simonis <[email protected]>
1 parent 25d4583 commit 7fc1c85

File tree

2 files changed

+80
-0
lines changed

2 files changed

+80
-0
lines changed

src/base/tests/test_util.py

+31
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,37 @@ def test_replace_record_references_batch__uniqueness(self):
10461046
[count] = self.env.cr.fetchone()
10471047
self.assertEqual(count, 1)
10481048

1049+
@unittest.skipUnless(util.version_gte("12.0"), "Only work on Odoo >= 12")
1050+
def test_remove_action_apply_cascade(self):
1051+
action = self.env["ir.actions.act_url"].create({"name": "act_test", "url": "test.com"})
1052+
self.env["ir.model.data"].create(
1053+
{"name": "act_test", "module": "base", "model": "ir.actions.act_url", "res_id": action.id}
1054+
)
1055+
1056+
filter_ = self.env["ir.filters"].create({"name": "filter", "model_id": "res.users", "action_id": action.id})
1057+
1058+
util.remove_record(self.env.cr, "base.act_test")
1059+
1060+
self.assertIsNone(util.ref(self.env.cr, "base.act_test"))
1061+
self.assertFalse(action.exists())
1062+
self.assertFalse(filter_.exists())
1063+
1064+
@unittest.skipUnless(util.version_gte("12.0"), "Only work on Odoo >= 12")
1065+
def test_remove_action_apply_setnull(self):
1066+
action = self.env["ir.actions.server"].create(
1067+
{"name": "act_test", "model_id": util.ref(self.env.cr, "base.model_res_users")}
1068+
)
1069+
1070+
user = self.env["res.users"].create({"login": "U1", "name": "U1", "action_id": action.id})
1071+
1072+
util.remove_action(self.env.cr, action_id=action.id)
1073+
1074+
util.invalidate(user)
1075+
1076+
self.assertFalse(action.exists())
1077+
self.assertTrue(user.exists())
1078+
self.assertFalse(user.action_id)
1079+
10491080

10501081
class TestMisc(UnitTestCase):
10511082
@parametrize(

src/util/records.py

+49
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,51 @@ def remove_asset(cr, name):
310310
# fmt:on
311311

312312

313+
def remove_action(cr, xml_id=None, action_id=None):
314+
assert bool(xml_id) ^ bool(action_id)
315+
316+
action_model = None
317+
if action_id:
318+
cr.execute("SELECT type FROM ir_actions WHERE id=%s", [action_id])
319+
[action_model] = cr.fetchone()
320+
else:
321+
module, _, name = xml_id.partition(".")
322+
cr.execute("SELECT model, res_id FROM ir_model_data WHERE module=%s AND name=%s", [module, name])
323+
action_model, action_id = cr.fetchone()
324+
if action_model not in {ihn.model for ihn in for_each_inherit(cr, "ir.actions.actions")}:
325+
raise ValueError(
326+
"%r should point to a model inheriting from 'ir.actions.actions', not a %r" % (xml_id, action_model)
327+
)
328+
329+
# on_delete value is correct only from version 11, thus can only be used on upgrades to version > 11
330+
if not version_gte("12.0"):
331+
return remove_records(cr, action_model, [action_id])
332+
333+
cr.execute(
334+
"""
335+
SELECT name,
336+
model,
337+
on_delete
338+
FROM ir_model_fields
339+
WHERE relation = 'ir.actions.actions'
340+
AND on_delete IN ( 'cascade', 'set null' )
341+
AND ttype = 'many2one'
342+
"""
343+
)
344+
345+
for column_name, model, on_delete in cr.fetchall():
346+
model_table = table_of_model(cr, model)
347+
if on_delete == "cascade":
348+
query = format_query(cr, "SELECT id FROM {} WHERE {} = %s", model_table, column_name)
349+
cr.execute(query, (action_id,))
350+
remove_records(cr, model, [id_ for (id_,) in cr.fetchall()])
351+
else:
352+
query = format_query(cr, "UPDATE {} SET {} = NULL WHERE {} = %s", model_table, column_name, column_name)
353+
explode_execute(cr, cr.mogrify(query, [action_id]).decode(), table=model_table)
354+
355+
return remove_records(cr, action_model, [action_id])
356+
357+
313358
def remove_record(cr, name):
314359
"""
315360
Remove a record and its references corresponding to the given :term:`xml_id <external identifier>`.
@@ -352,6 +397,10 @@ def remove_record(cr, name):
352397
_logger.log(NEARLYWARN, "Removing group %r", name)
353398
return remove_group(cr, group_id=res_id)
354399

400+
if model in {ihn.model for ihn in for_each_inherit(cr, "ir.actions.actions")}:
401+
_logger.log(NEARLYWARN, "Removing action %r", name)
402+
return remove_action(cr, action_id=res_id)
403+
355404
return remove_records(cr, model, [res_id])
356405

357406

0 commit comments

Comments
 (0)