From ccbb2a85979e7bd52d8d111801c8e7171d7883c6 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 26 Aug 2026 04:37:21 -0400 Subject: [PATCH] =?UTF-8?q?[FIX]=20liste=20de=20prix=20:=20ne=20rien=20cr?= =?UTF-8?q?=C3=A9er=20quand=20il=20en=20existe=20d=C3=A9j=C3=A0=20une?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_activate_or_create_pricelists` ne compte pas une liste de prix partagée entre sociétés (company_id vide) comme appartenant à la société : elle voit une société « sans liste » et lui en fabrique une. Sur une base migrée qui n'a rien perdu, cela ajoute un doublon vide à côté de la liste existante. Elle interroge maintenant le détecteur `pricelist_missing` du même outil, qui distingue déjà les deux cas. Vérifié dans les deux sens sur copie jetable : liste présente, rien n'est créé ; aucune liste, la réparation en crée une. --- EN --- `_activate_or_create_pricelists` does not count a pricelist shared across companies (empty company_id) as belonging to the company: it sees a company "without a pricelist" and makes one. On a migrated database that lost nothing, that adds an empty duplicate beside the existing list. It now asks the same tool's `pricelist_missing` detector, which already tells the two cases apart. Verified both ways on a throwaway copy: a pricelist present, nothing is created; no pricelist, the repair creates one. Assisted-by: Claude Opus 5 (cherry picked from commit b5bd2e4e44e89bdba4d639672611dcb964265987) --- .../odoo/migration/restore_config_defaults.py | 10 +++- test/test_monitoring.py | 53 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/script/odoo/migration/restore_config_defaults.py b/script/odoo/migration/restore_config_defaults.py index ff932b8..e887351 100755 --- a/script/odoo/migration/restore_config_defaults.py +++ b/script/odoo/migration/restore_config_defaults.py @@ -90,7 +90,15 @@ try: rapport["pricelist_group"] = bool( fonction and employe and fonction in employe.implied_ids ) - if not DRY and rapport["pricelist_group"]: + # Et seulement s'il n'y en a AUCUNE. `_activate_or_create_pricelists` + # ne compte pas une liste PARTAGÉE (company_id vide) comme + # appartenant à la société : appelée alors qu'une liste existe, elle + # en ajoute une seconde. Mesuré sur une migration où la liste avait + # traversé les six paliers — la réparation a créé un doublon vide à + # côté d'elle. Cet outil a son détecteur ; il le consulte. + if not DRY and rapport["pricelist_group"] and not rapport[ + "pricelist_before" + ]: Societe._activate_or_create_pricelists() env.cr.commit() rapport["pricelist_after"] = Liste.search_count([]) diff --git a/test/test_monitoring.py b/test/test_monitoring.py index 0870edb..3f0a880 100644 --- a/test/test_monitoring.py +++ b/test/test_monitoring.py @@ -383,3 +383,56 @@ class TestAMissingPricelistOnlyCountsWhenTheFeatureIsOn(unittest.TestCase): sql = " ".join(self._controle()["sql"].split()) self.assertIn("ir_module_module", sql) self.assertIn("to_regclass", sql) + + +class TestTheRepairAsksItsOwnDetector(unittest.TestCase): + """Une réparation qui n'écoute pas son détecteur fabrique des doublons. + + Mesuré sur une migration de bout en bout : la liste de prix avait + traversé les six paliers, PARTAGÉE entre sociétés (company_id vide). + `_activate_or_create_pricelists` ne compte pas une liste partagée + comme appartenant à la société — elle en a donc créé une seconde, + vide, à côté de celle du client. + + Le détecteur `pricelist_missing`, lui, disait déjà « rien ne manque ». + Il fallait que la réparation le lui demande. + """ + + def _source(self): + from pathlib import Path + + chemin = ( + Path(__file__).resolve().parent.parent + / "script" + / "odoo" + / "migration" + / "restore_config_defaults.py" + ) + return chemin.read_text(encoding="utf-8") + + def _garde(self): + """Le texte qui précède l'APPEL, pas sa mention dans la docstring. + + `index` trouvait la PREMIÈRE occurrence — celle de l'en-tête du + module, qui explique justement ce que fait cette méthode. Le test + s'évaluait alors sur un extrait de prose et échouait. `rindex` + prend la dernière, qui est l'appel. + """ + source = self._source() + debut = source.rindex("_activate_or_create_pricelists()") + return source[max(0, debut - 400) : debut] + + def test_the_repair_is_gated_on_an_empty_count(self): + self.assertIn("pricelist_before", self._garde()) + + def test_the_feature_condition_is_still_there(self): + self.assertIn("pricelist_group", self._garde()) + + def test_the_feature_is_read_from_the_implication(self): + """Et non de l'appartenance de celui qui exécute.""" + source = self._source() + self.assertIn("implied_ids", source) + self.assertNotIn( + 'env.user.has_group(\n "product.group_product_pricelist"', + source, + )