diff --git a/script/analyse/check_migration_residue.py b/script/analyse/check_migration_residue.py index f2ef0e3..b2320ea 100755 --- a/script/analyse/check_migration_residue.py +++ b/script/analyse/check_migration_residue.py @@ -139,9 +139,22 @@ CONTROLES = ( # chercher l'xmlid signalait donc une base parfaitement saine, et # aurait signalé de même celle d'un client qui a créé la sienne à # la main. + # + # Et seulement si la FONCTIONNALITÉ est active, c'est-à-dire si + # `base.group_user` implique `product.group_product_pricelist` — + # la question exacte que pose la case des réglages. Sans elle, + # l'absence de liste est normale ; signaler quand même menait à + # créer une liste dans une base qui n'en veut pas, et Odoo + # prévenait alors à chaque ouverture des réglages qu'il allait + # l'archiver. "sql": "SELECT CASE WHEN EXISTS (SELECT 1 FROM ir_module_module" " WHERE name='product' AND state='installed')" " AND to_regclass('public.product_pricelist') IS NOT NULL" + " AND EXISTS (SELECT 1 FROM res_groups_implied_rel r" + " JOIN ir_model_data u ON u.model='res.groups' AND u.res_id=r.gid" + " AND u.module='base' AND u.name='group_user'" + " JOIN ir_model_data g ON g.model='res.groups' AND g.res_id=r.hid" + " AND g.module='product' AND g.name='group_product_pricelist')" " AND NOT EXISTS (SELECT 1 FROM product_pricelist)" " THEN 1 ELSE 0 END", "gravity": "broken", diff --git a/script/odoo/migration/restore_config_defaults.py b/script/odoo/migration/restore_config_defaults.py index e40cf92..ff932b8 100755 --- a/script/odoo/migration/restore_config_defaults.py +++ b/script/odoo/migration/restore_config_defaults.py @@ -74,10 +74,21 @@ try: if "product.pricelist" in env: Liste = env["product.pricelist"].sudo().with_context(active_test=False) rapport["pricelist_before"] = Liste.search_count([]) - # Le groupe décide : sans lui la fonctionnalité est masquée et - # l'absence de liste est normale, pas une perte. - rapport["pricelist_group"] = env.user.has_group( - "product.group_product_pricelist" + # Ce qui décide, c'est la FONCTIONNALITÉ, pas l'appartenance de + # celui qui exécute. `has_group` était vrai ici parce que six + # utilisateurs sont membres directs du groupe — hérité d'un + # palier de migration — alors que la case de configuration était + # décochée. On créait donc une liste de prix dans une base dont + # la fonctionnalité est éteinte, et Odoo prévenait à chaque + # ouverture des réglages qu'il allait l'archiver. + # + # `res.config.settings` lit ce que `base.group_user` IMPLIQUE + # (res_config.py : « which groups are implied by the group + # Employee ») : c'est la même question qu'on pose ici. + fonction = env.ref("product.group_product_pricelist", False) + employe = env.ref("base.group_user", False) + rapport["pricelist_group"] = bool( + fonction and employe and fonction in employe.implied_ids ) if not DRY and rapport["pricelist_group"]: Societe._activate_or_create_pricelists() diff --git a/test/test_monitoring.py b/test/test_monitoring.py index eb6e9a7..0870edb 100644 --- a/test/test_monitoring.py +++ b/test/test_monitoring.py @@ -346,3 +346,40 @@ class TestTheSourceMenuIsWrittenTwice(unittest.TestCase): """La provenance la plus directe, et l'ordre du menu d'à côté.""" self.assertEqual(self.affiches[0], "1") self.assertIn("A local database", self.corps.split("if answer")[0]) + + +class TestAMissingPricelistOnlyCountsWhenTheFeatureIsOn(unittest.TestCase): + """Une liste de prix absente n'est un défaut que si on l'a demandée. + + Mesuré : six utilisateurs étaient membres DIRECTS de + `product.group_product_pricelist` — hérité d'un palier de migration — + alors que `base.group_user` ne l'impliquait pas. La case des réglages + était donc décochée, et la réparation créait quand même une liste. + Odoo prévenait ensuite à chaque ouverture des réglages qu'il allait + l'archiver. + + `res.config.settings` lit ce que `base.group_user` IMPLIQUE ; le + contrôle pose désormais la même question. + """ + + def _controle(self): + for controle in residue.CONTROLES: + if controle["key"] == "missing_pricelist": + return controle + raise AssertionError("contrôle introuvable") + + def test_it_asks_whether_the_feature_is_implied(self): + sql = " ".join(self._controle()["sql"].split()) + self.assertIn("res_groups_implied_rel", sql) + self.assertIn("group_product_pricelist", sql) + self.assertIn("group_user", sql) + + def test_it_does_not_settle_for_direct_membership(self): + """`res_groups_users_rel` dirait « quelqu'un est dans le groupe », + ce qui était vrai et menait au faux positif.""" + self.assertNotIn("res_groups_users_rel", self._controle()["sql"]) + + def test_it_still_requires_the_module_and_the_table(self): + sql = " ".join(self._controle()["sql"].split()) + self.assertIn("ir_module_module", sql) + self.assertIn("to_regclass", sql)