[FIX] liste de prix : ne rien créer quand il en existe déjà une

`_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)
This commit is contained in:
Mathieu Benoit 2026-08-26 04:37:21 -04:00
parent 2534d4d022
commit ccbb2a8597
2 changed files with 62 additions and 1 deletions

View file

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

View file

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