From a7d2f12a23b410773f2756bf3b7a45accb1a413b Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Fri, 14 Nov 2025 16:37:09 +0100 Subject: [PATCH] [IMP] util.delete_unused Allow to include (some) m2m tables in the search of usage of records. It can be useful to include m2m when removing records that are expected to be used as m2m, like tags. --- src/base/tests/test_util.py | 18 ++++++++++++++++++ src/util/records.py | 8 +++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/base/tests/test_util.py b/src/base/tests/test_util.py index 01f9c0e23..424b8153a 100644 --- a/src/base/tests/test_util.py +++ b/src/base/tests/test_util.py @@ -1942,6 +1942,24 @@ def test_delete_unused_multi_cascade_fk(self): self.assertTrue(cat_2.exists()) self.assertTrue(cat_3.exists()) + def test_delete_unused_include_m2m(self): + cat_1, cat_2, cat_3 = self._prepare_test_delete_unused() + + cr = self.env.cr + cr.execute( + "INSERT INTO res_partner_res_partner_category_rel(partner_id, category_id) VALUES(%s, %s)", + [util.ref(cr, "base.partner_root"), cat_2.id], + ) + + deleted = util.delete_unused( + self.env.cr, f"base.{cat_1.name}", f"base.{cat_2.name}", f"base.{cat_3.name}", include_m2m="*" + ) + + self.assertEqual(deleted, [f"base.{cat_3.name}"]) + self.assertTrue(cat_1.exists()) + self.assertTrue(cat_2.exists()) + self.assertFalse(cat_3.exists()) + class TestEditView(UnitTestCase): @parametrize( diff --git a/src/util/records.py b/src/util/records.py index ce89b3872..7036bde5c 100644 --- a/src/util/records.py +++ b/src/util/records.py @@ -49,6 +49,7 @@ format_query, get_columns, get_fk, + get_m2m_tables, get_value_or_en_translation, parallel_execute, table_exists, @@ -1282,11 +1283,14 @@ def delete_unused(cr, *xmlids, **kwargs): :param bool keep_xmlids: whether to keep the xml_ids of records that cannot be removed. By default `True` for versions up to 18.0, `False` from `saas~18.1` on. + :param list(str) or str include_m2m: list of m2m tables to include in the search. + `"*"` for all. :return: list of ids of removed records, if any :rtype: list(int) """ deactivate = kwargs.pop("deactivate", False) keep_xmlids = kwargs.pop("keep_xmlids", not version_gte("saas~18.1")) + include_m2m = kwargs.pop("include_m2m", ()) if kwargs: raise TypeError("delete_unused() got an unexpected keyword argument %r" % kwargs.popitem()[0]) @@ -1358,12 +1362,14 @@ def delete_unused(cr, *xmlids, **kwargs): else: kids_query = format_query(cr, "SELECT id, ARRAY[id] AS children FROM {0} WHERE id = ANY(%(ids)s)", table) + m2m_tables = include_m2m if include_m2m != "*" else get_m2m_tables(cr, table) + sub = " UNION ALL ".join( [ format_query(cr, "SELECT 1 FROM {} x WHERE x.{} = ANY(s.children)", fk_tbl, fk_col) for fk_tbl, fk_col, _, fk_act in get_fk(cr, table, quote_ident=False) # ignore "on delete cascade" fk (they are indirect dependencies (lines or m2m)) - if fk_act != "c" + if (fk_act != "c" or fk_tbl in m2m_tables) # ignore children records unless the deletion is restricted if not (fk_tbl == table and fk_act != "r") ]