[FIX] liste de prix : décider sur la fonctionnalité, pas sur l'exécutant

`env.user.has_group()` répond « oui » dès que l'exécutant est membre du
groupe, et la migration l'y ajoute en cours de route. Or la case des
réglages lit tout autre chose : ce que `base.group_user` IMPLIQUE
(res_config.py, « which groups are implied by the group Employee »).
Décider sur l'exécutant créait 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. Le contrôle « restant de migration »
posait la même mauvaise question ; les deux lisent désormais l'implication
du groupe. Vérifié sur copie jetable, dans les deux sens : fonctionnalité
éteinte, rien n'est signalé ; activée, constat et réparation reviennent.

--- EN ---

`env.user.has_group()` says yes as soon as the caller belongs to the
group, and the migration adds it along the way. But the settings checkbox
reads something else: what `base.group_user` IMPLIES (res_config.py,
"which groups are implied by the group Employee"). Deciding on the caller
created a pricelist in a database whose feature is off, and Odoo warned on
every opening of the settings that it would archive it. The
migration-residue check asked the same wrong question; both now read the
group implication. Verified on a throwaway copy, both ways: feature off,
nothing is reported; feature on, finding and repair come back.

Assisted-by: Claude Opus 5
(cherry picked from commit 38ba25e01894f45fb956bcbb08cc3d96a125e648)
This commit is contained in:
Mathieu Benoit 2026-08-26 03:58:45 -04:00
parent 8cc5566743
commit 2534d4d022
3 changed files with 65 additions and 4 deletions

View file

@ -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",

View file

@ -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()

View file

@ -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)