[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:
parent
2534d4d022
commit
ccbb2a8597
2 changed files with 62 additions and 1 deletions
|
|
@ -90,7 +90,15 @@ try:
|
||||||
rapport["pricelist_group"] = bool(
|
rapport["pricelist_group"] = bool(
|
||||||
fonction and employe and fonction in employe.implied_ids
|
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()
|
Societe._activate_or_create_pricelists()
|
||||||
env.cr.commit()
|
env.cr.commit()
|
||||||
rapport["pricelist_after"] = Liste.search_count([])
|
rapport["pricelist_after"] = Liste.search_count([])
|
||||||
|
|
|
||||||
|
|
@ -383,3 +383,56 @@ class TestAMissingPricelistOnlyCountsWhenTheFeatureIsOn(unittest.TestCase):
|
||||||
sql = " ".join(self._controle()["sql"].split())
|
sql = " ".join(self._controle()["sql"].split())
|
||||||
self.assertIn("ir_module_module", sql)
|
self.assertIn("ir_module_module", sql)
|
||||||
self.assertIn("to_regclass", 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,
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue