[ADD] migration: recréer les réglages qu'aucun événement ne remet
Deux enregistrements ont cessé d'être LIVRÉS en données pour devenir le produit d'un geste : créer une société, cocher une case, charger un plan comptable. Une migration n'en fait aucun. Le nettoyage des orphelins les emporte et rien ne les remet. `product.list0` est déclaré jusqu'en 16 : 1 liste en 16, 0 en 17, alors que le groupe compte six membres — tout devis s'ouvre alors sans liste de prix. Le modèle de rapprochement est déclaré en 12 seulement : 0 dès la 13, pour trois journaux de trésorerie. On n'invente rien : `_activate_or_create_pricelists` et `_load_data` du plan comptable, les méthodes qu'Odoo aurait appelées. Éprouvé sur copie : 0 → 1 liste, 0 → 4 modèles, un devis reprend « Par défaut (CAD) », un second passage ne double rien. --- EN --- Two records stopped being SHIPPED as data and became the product of a gesture: creating a company, ticking a box, loading a chart of accounts. A migration does none of them. The orphan cleanup takes them away and nothing brings them back. `product.list0` is declared up to 16: 1 pricelist in 16, 0 in 17, while the group has six members — every quotation then opens without a pricelist. The reconciliation model is declared in 12 only: 0 from 13 on, for three cash journals. Nothing is invented: `_activate_or_create_pricelists` and the chart template's `_load_data`, the methods Odoo itself would have called. Proven on a copy: 0 → 1 pricelist, 0 → 4 models, a quotation picks up « Par défaut (CAD) », a second pass duplicates nothing. Assisted-by: Claude Opus 5
This commit is contained in:
parent
93d26f50cb
commit
721e59dba9
2 changed files with 459 additions and 0 deletions
262
script/odoo/migration/restore_config_defaults.py
Executable file
262
script/odoo/migration/restore_config_defaults.py
Executable file
|
|
@ -0,0 +1,262 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
"""Les réglages qu'Odoo ne recrée qu'à des moments qu'une migration évite.
|
||||||
|
|
||||||
|
Certains enregistrements de configuration ont cessé d'être LIVRÉS en
|
||||||
|
données et sont devenus le produit d'un geste : créer une société,
|
||||||
|
changer de devise, cocher une case, charger un plan comptable. Une
|
||||||
|
migration ne fait aucun de ces gestes. L'enregistrement part avec le
|
||||||
|
nettoyage des orphelins et rien ne le remet.
|
||||||
|
|
||||||
|
Deux cas mesurés sur une chaîne 12 → 18 réelle :
|
||||||
|
|
||||||
|
liste de prix `product.list0` est déclaré jusqu'en 16 et plus
|
||||||
|
après. Odoo 18 crée les listes par défaut dans
|
||||||
|
`_activate_or_create_pricelists()`, appelée à la
|
||||||
|
création d'une société, au changement de devise
|
||||||
|
et au basculement du réglage. Résultat : 1 liste
|
||||||
|
en 16, 0 en 17 et en 18, alors que le groupe
|
||||||
|
`product.group_product_pricelist` compte six
|
||||||
|
membres. Toute commande s'ouvre alors sans liste
|
||||||
|
de prix — elle ne casse rien, elle facture faux.
|
||||||
|
|
||||||
|
rapprochement bancaire `account.reconciliation_model_default_rule` est
|
||||||
|
déclaré en 12 et plus dès la 13 : la 18 le crée
|
||||||
|
depuis le PLAN COMPTABLE
|
||||||
|
(`_get_account_reconcile_model`), qu'une montée
|
||||||
|
de version ne recharge jamais. Résultat : 1 en
|
||||||
|
12, 0 partout ensuite, et plus aucun lettrage
|
||||||
|
automatique sur les journaux de trésorerie.
|
||||||
|
|
||||||
|
On n'invente rien : on appelle les méthodes d'Odoo, celles-là mêmes que
|
||||||
|
le geste manquant aurait appelées. `_load_data` est rejoué avec
|
||||||
|
`ignore_duplicates`, donc l'outil se relance sans risque.
|
||||||
|
|
||||||
|
Lecture seule par défaut. `--apply` écrit, puis RELIT pour vérifier.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.append(
|
||||||
|
os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||||
|
)
|
||||||
|
|
||||||
|
import database_cleanup # noqa: E402
|
||||||
|
|
||||||
|
try:
|
||||||
|
from script.todo.todo_i18n import t
|
||||||
|
except Exception: # pragma: no cover - repli si i18n indisponible
|
||||||
|
|
||||||
|
def t(key: str) -> str:
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
# Les sentinelles de `run_shell`, pas les nôtres : il les impose, et en
|
||||||
|
# poser d'autres ferait lire un rapport vide.
|
||||||
|
DEBUT = database_cleanup.START
|
||||||
|
FIN = database_cleanup.END
|
||||||
|
|
||||||
|
SCRIPT = """
|
||||||
|
import json
|
||||||
|
DRY = {dry}
|
||||||
|
rapport = {{"dry_run": DRY}}
|
||||||
|
try:
|
||||||
|
Societe = env["res.company"].sudo()
|
||||||
|
societes = Societe.search([])
|
||||||
|
rapport["companies"] = len(societes)
|
||||||
|
|
||||||
|
# ── Listes de prix ──────────────────────────────────────────────
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
if not DRY and rapport["pricelist_group"]:
|
||||||
|
Societe._activate_or_create_pricelists()
|
||||||
|
env.cr.commit()
|
||||||
|
rapport["pricelist_after"] = Liste.search_count([])
|
||||||
|
else:
|
||||||
|
rapport["pricelist_absent"] = True
|
||||||
|
|
||||||
|
# ── Modèles de rapprochement bancaire ───────────────────────────
|
||||||
|
if "account.reconcile.model" in env:
|
||||||
|
Modele = env["account.reconcile.model"].sudo()
|
||||||
|
rapport["reconcile_before"] = Modele.search_count([])
|
||||||
|
# Sans journal de trésorerie il n'y a rien à rapprocher : créer
|
||||||
|
# des modèles là serait du bruit dans un menu qu'on n'ouvre pas.
|
||||||
|
rapport["cash_journals"] = env["account.journal"].sudo().search_count(
|
||||||
|
[("type", "in", ("bank", "cash"))]
|
||||||
|
)
|
||||||
|
rapport["charts"] = sorted(
|
||||||
|
{{s.chart_template for s in societes if s.chart_template}}
|
||||||
|
)
|
||||||
|
if not DRY and rapport["cash_journals"] and rapport["charts"]:
|
||||||
|
Gabarit = env["account.chart.template"].sudo()
|
||||||
|
for societe in societes:
|
||||||
|
if not societe.chart_template:
|
||||||
|
continue
|
||||||
|
donnees = Gabarit._get_account_reconcile_model(
|
||||||
|
societe.chart_template
|
||||||
|
)
|
||||||
|
if not donnees:
|
||||||
|
continue
|
||||||
|
# `_load_data` est le chemin d'Odoo : il pose les xmlid
|
||||||
|
# préfixés par la société, donc un rechargement futur du
|
||||||
|
# plan comptable ne fera pas de doublon.
|
||||||
|
Gabarit.with_company(societe)._load_data(
|
||||||
|
{{"account.reconcile.model": donnees}},
|
||||||
|
ignore_duplicates=True,
|
||||||
|
)
|
||||||
|
env.cr.commit()
|
||||||
|
rapport["reconcile_after"] = Modele.search_count([])
|
||||||
|
else:
|
||||||
|
rapport["reconcile_absent"] = True
|
||||||
|
except Exception as exc:
|
||||||
|
rapport["error"] = "%s: %s" % (type(exc).__name__, exc)
|
||||||
|
print({debut!r})
|
||||||
|
print(json.dumps(rapport))
|
||||||
|
print({fin!r})
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def build_script(dry_run):
|
||||||
|
return SCRIPT.format(
|
||||||
|
dry="True" if dry_run else "False", debut=DEBUT, fin=FIN
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pricelist_missing(rapport, apres=False):
|
||||||
|
"""Une liste manque-t-elle vraiment ?
|
||||||
|
|
||||||
|
Pas de liste ET le groupe actif : la fonctionnalité est offerte aux
|
||||||
|
utilisateurs et ne répond rien. Groupe éteint, l'absence est normale.
|
||||||
|
|
||||||
|
`apres` regarde le compte D'APRÈS la réparation. Sans ce choix, un
|
||||||
|
`--apply` réussi se conclurait toujours par « il en manque encore » :
|
||||||
|
le compte d'avant, lui, reste à zéro pour l'éternité.
|
||||||
|
"""
|
||||||
|
if rapport.get("pricelist_absent"):
|
||||||
|
return False
|
||||||
|
if not rapport.get("pricelist_group"):
|
||||||
|
return False
|
||||||
|
cle = "pricelist_after" if apres else "pricelist_before"
|
||||||
|
return not rapport.get(cle)
|
||||||
|
|
||||||
|
|
||||||
|
def reconcile_missing(rapport, apres=False):
|
||||||
|
"""Aucun modèle alors qu'il y a de quoi rapprocher."""
|
||||||
|
if rapport.get("reconcile_absent"):
|
||||||
|
return False
|
||||||
|
if not rapport.get("cash_journals"):
|
||||||
|
return False
|
||||||
|
cle = "reconcile_after" if apres else "reconcile_before"
|
||||||
|
return not rapport.get(cle)
|
||||||
|
|
||||||
|
|
||||||
|
def findings(rapport, apres=False):
|
||||||
|
lst = []
|
||||||
|
if pricelist_missing(rapport, apres):
|
||||||
|
lst.append("pricelist")
|
||||||
|
if reconcile_missing(rapport, apres):
|
||||||
|
lst.append("reconcile")
|
||||||
|
return lst
|
||||||
|
|
||||||
|
|
||||||
|
def render(rapport, dry_run):
|
||||||
|
lignes = []
|
||||||
|
if not findings(rapport):
|
||||||
|
return [f"✅ {t('Every default configuration record is there.')}"]
|
||||||
|
lignes.append(
|
||||||
|
f"🧾 {t('Configuration records a migration never recreates')}"
|
||||||
|
)
|
||||||
|
lignes.append("")
|
||||||
|
if pricelist_missing(rapport):
|
||||||
|
lignes.append(
|
||||||
|
f" 💲 {t('No pricelist, and the pricelist feature is on.')}"
|
||||||
|
)
|
||||||
|
lignes.append(
|
||||||
|
f" {t('Every quotation opens without one: raw price, no rule.')}"
|
||||||
|
)
|
||||||
|
if reconcile_missing(rapport):
|
||||||
|
lignes.append(
|
||||||
|
f" 🏦 {t('No bank reconciliation model, for')}"
|
||||||
|
f" {rapport.get('cash_journals', 0)} {t('cash/bank journal(s).')}"
|
||||||
|
)
|
||||||
|
lignes.append(f" {t('Every statement line is matched by hand.')}")
|
||||||
|
lignes.append("")
|
||||||
|
if dry_run:
|
||||||
|
lignes.append(f" {t('Use --apply to let Odoo recreate them.')}")
|
||||||
|
else:
|
||||||
|
lignes.append(
|
||||||
|
f" {t('pricelists:')} {rapport.get('pricelist_before', 0)}"
|
||||||
|
f" → {rapport.get('pricelist_after', 0)}"
|
||||||
|
f" · {t('reconcile models:')}"
|
||||||
|
f" {rapport.get('reconcile_before', 0)}"
|
||||||
|
f" → {rapport.get('reconcile_after', 0)}"
|
||||||
|
)
|
||||||
|
return lignes
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=(
|
||||||
|
"Report the default configuration records a version bump drops"
|
||||||
|
" and no event recreates, and optionally let Odoo recreate them."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument("-d", "--database", required=True)
|
||||||
|
parser.add_argument("-c", "--config", default="config.conf")
|
||||||
|
parser.add_argument(
|
||||||
|
"--apply",
|
||||||
|
action="store_true",
|
||||||
|
help="actually recreate them (default: report only)",
|
||||||
|
)
|
||||||
|
config = parser.parse_args(argv)
|
||||||
|
|
||||||
|
# Le même garde-fou que partout : un Odoo d'une autre version ÉCRIT
|
||||||
|
# dans la base avant d'échouer.
|
||||||
|
souci = database_cleanup.require_matching_version(config.database)
|
||||||
|
if souci:
|
||||||
|
print(f"❌ {souci}")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
try:
|
||||||
|
rapport = database_cleanup.run_shell(
|
||||||
|
config.database,
|
||||||
|
config.config,
|
||||||
|
build_script(not config.apply),
|
||||||
|
echo=lambda texte: print(f"⧖ {texte}", flush=True),
|
||||||
|
)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(f"❌ {exc}")
|
||||||
|
return 2
|
||||||
|
if rapport.get("error"):
|
||||||
|
print(f"❌ {rapport['error']}")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
print("\n".join(render(rapport, not config.apply)))
|
||||||
|
if not findings(rapport):
|
||||||
|
return 0
|
||||||
|
if not config.apply:
|
||||||
|
return 1
|
||||||
|
# Juger sur le compte D'APRÈS : le script a recompté dans la même
|
||||||
|
# session. Annoncer « recréé » sans regarder ferait découvrir
|
||||||
|
# l'absence au premier devis.
|
||||||
|
reste = findings(rapport, apres=True)
|
||||||
|
if reste:
|
||||||
|
print(f"⚠️ {t('Still missing after the repair.')} {', '.join(reste)}")
|
||||||
|
return 1
|
||||||
|
print(f"✅ {t('Odoo recreated them.')}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
197
test/test_restore_config_defaults.py
Normal file
197
test/test_restore_config_defaults.py
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
"""Les réglages qu'Odoo ne recrée qu'à des moments qu'une migration évite.
|
||||||
|
|
||||||
|
Deux enregistrements ont cessé d'être LIVRÉS en données pour devenir le
|
||||||
|
produit d'un geste — créer une société, cocher une case, charger un plan
|
||||||
|
comptable. Une migration n'en fait aucun : le nettoyage des orphelins les
|
||||||
|
emporte et rien ne les remet.
|
||||||
|
|
||||||
|
Mesuré sur une chaîne 12 → 18 réelle : 1 liste de prix en 16, 0 en 17,
|
||||||
|
alors que le groupe compte six membres ; 1 modèle de rapprochement en 12,
|
||||||
|
0 dès la 13, pour trois journaux de trésorerie.
|
||||||
|
|
||||||
|
Ce que l'outil doit surtout savoir faire, c'est SE TAIRE : sans le groupe
|
||||||
|
des listes de prix, sans journal de trésorerie, ou sur une base qui n'a
|
||||||
|
ni vente ni comptabilité, l'absence est normale et le dire serait du
|
||||||
|
bruit qu'on apprendrait à ignorer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.append(
|
||||||
|
os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
)
|
||||||
|
|
||||||
|
from script.odoo.migration import restore_config_defaults as conf # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def rapport(**extra):
|
||||||
|
base = {
|
||||||
|
"dry_run": True,
|
||||||
|
"companies": 1,
|
||||||
|
"pricelist_before": 0,
|
||||||
|
"pricelist_after": 0,
|
||||||
|
"pricelist_group": True,
|
||||||
|
"reconcile_before": 0,
|
||||||
|
"reconcile_after": 0,
|
||||||
|
"cash_journals": 3,
|
||||||
|
"charts": ["ca_2023"],
|
||||||
|
}
|
||||||
|
base.update(extra)
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
class TestWhenAPricelistIsReallyMissing(unittest.TestCase):
|
||||||
|
def test_none_and_the_feature_is_on(self):
|
||||||
|
self.assertTrue(conf.pricelist_missing(rapport()))
|
||||||
|
|
||||||
|
def test_the_feature_is_off_so_the_absence_is_normal(self):
|
||||||
|
# Sans le groupe, le menu n'existe pas : signaler l'absence
|
||||||
|
# enverrait réparer ce que personne n'utilise.
|
||||||
|
self.assertFalse(
|
||||||
|
conf.pricelist_missing(rapport(pricelist_group=False))
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_there_is_already_one(self):
|
||||||
|
self.assertFalse(conf.pricelist_missing(rapport(pricelist_before=1)))
|
||||||
|
|
||||||
|
def test_the_module_is_not_installed_at_all(self):
|
||||||
|
self.assertFalse(
|
||||||
|
conf.pricelist_missing(rapport(pricelist_absent=True))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWhenAReconcileModelIsReallyMissing(unittest.TestCase):
|
||||||
|
def test_none_and_there_are_journals_to_match(self):
|
||||||
|
self.assertTrue(conf.reconcile_missing(rapport()))
|
||||||
|
|
||||||
|
def test_no_cash_journal_so_nothing_to_match(self):
|
||||||
|
# Créer des modèles là serait du bruit dans un menu qu'on
|
||||||
|
# n'ouvre pas.
|
||||||
|
self.assertFalse(conf.reconcile_missing(rapport(cash_journals=0)))
|
||||||
|
|
||||||
|
def test_there_are_already_some(self):
|
||||||
|
self.assertFalse(conf.reconcile_missing(rapport(reconcile_before=4)))
|
||||||
|
|
||||||
|
def test_accounting_is_not_installed(self):
|
||||||
|
self.assertFalse(
|
||||||
|
conf.reconcile_missing(rapport(reconcile_absent=True))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestJudgingBeforeAndAfter(unittest.TestCase):
|
||||||
|
"""Le compte d'AVANT reste à zéro pour l'éternité.
|
||||||
|
|
||||||
|
Juger dessus après une réparation réussie conclurait toujours « il en
|
||||||
|
manque encore ». C'est le compte d'APRÈS qui dit si le geste a pris.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_before_the_repair_both_are_missing(self):
|
||||||
|
self.assertEqual(["pricelist", "reconcile"], conf.findings(rapport()))
|
||||||
|
|
||||||
|
def test_after_a_successful_repair_nothing_is_missing(self):
|
||||||
|
plein = rapport(pricelist_after=1, reconcile_after=4)
|
||||||
|
self.assertEqual([], conf.findings(plein, apres=True))
|
||||||
|
# …et le compte d'avant, lui, dit toujours qu'il manquait.
|
||||||
|
self.assertEqual(2, len(conf.findings(plein)))
|
||||||
|
|
||||||
|
def test_a_half_repair_is_reported(self):
|
||||||
|
moitie = rapport(pricelist_after=1, reconcile_after=0)
|
||||||
|
self.assertEqual(["reconcile"], conf.findings(moitie, apres=True))
|
||||||
|
|
||||||
|
|
||||||
|
class TestTheReport(unittest.TestCase):
|
||||||
|
def test_a_healthy_database_says_so(self):
|
||||||
|
sain = rapport(pricelist_before=1, reconcile_before=4)
|
||||||
|
texte = "\n".join(conf.render(sain, True))
|
||||||
|
self.assertIn("✅", texte)
|
||||||
|
|
||||||
|
def test_it_says_what_breaks_for_a_user_not_what_is_missing(self):
|
||||||
|
# « 0 liste de prix » ne dit rien à personne ; « tout devis
|
||||||
|
# s'ouvre sans liste » se comprend sans connaître le modèle.
|
||||||
|
#
|
||||||
|
# On demande la MÊME traduction que le code : comparer à un mot
|
||||||
|
# anglais en dur fait tomber le test dès qu'on traduit, ce qui
|
||||||
|
# m'est arrivé sur celui-ci.
|
||||||
|
texte = "\n".join(conf.render(rapport(), True))
|
||||||
|
self.assertIn(
|
||||||
|
conf.t("Every quotation opens without one: raw price, no rule."),
|
||||||
|
texte,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
conf.t("Every statement line is matched by hand."), texte
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_it_counts_the_journals_it_found(self):
|
||||||
|
texte = "\n".join(conf.render(rapport(cash_journals=3), True))
|
||||||
|
self.assertIn("3", texte)
|
||||||
|
|
||||||
|
def test_the_dry_run_offers_the_flag(self):
|
||||||
|
self.assertIn("--apply", "\n".join(conf.render(rapport(), True)))
|
||||||
|
|
||||||
|
def test_after_applying_it_shows_the_movement(self):
|
||||||
|
# Un rapport qui répète « il en manquait » sans dire ce qui a été
|
||||||
|
# fait laisse rouvrir l'outil pour savoir.
|
||||||
|
fait = rapport(pricelist_after=1, reconcile_after=4)
|
||||||
|
texte = "\n".join(conf.render(fait, False))
|
||||||
|
self.assertIn("0 → 1", texte)
|
||||||
|
self.assertIn("0 → 4", texte)
|
||||||
|
self.assertNotIn("--apply", texte)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTheOrmScript(unittest.TestCase):
|
||||||
|
"""Le script poussé dans le shell doit être du Python valide."""
|
||||||
|
|
||||||
|
def test_both_shapes_compile(self):
|
||||||
|
for sec in (True, False):
|
||||||
|
with self.subTest(dry=sec):
|
||||||
|
compile(conf.build_script(sec), "script", "exec")
|
||||||
|
|
||||||
|
def test_the_dry_run_never_writes(self):
|
||||||
|
sec = conf.build_script(True)
|
||||||
|
self.assertIn("DRY = True", sec)
|
||||||
|
|
||||||
|
def test_applying_commits_each_repair(self):
|
||||||
|
# Le shell d'Odoo annule en sortant : sans commit, la réparation
|
||||||
|
# s'évapore et l'outil annonce une réussite qui n'a rien laissé.
|
||||||
|
#
|
||||||
|
# UN par réparation, et l'on compte : chercher « il y a un
|
||||||
|
# commit » laissait retirer celui du rapprochement sans que rien
|
||||||
|
# ne le dise — l'autre le masquait. Mesuré.
|
||||||
|
vif = conf.build_script(False)
|
||||||
|
self.assertIn("DRY = False", vif)
|
||||||
|
self.assertEqual(2, vif.count("env.cr.commit()"), vif)
|
||||||
|
|
||||||
|
def test_each_branch_has_its_own_commit(self):
|
||||||
|
vif = conf.build_script(False)
|
||||||
|
prix = vif[vif.index("_activate_or_create_pricelists") :]
|
||||||
|
prix = prix[: prix.index("account.reconcile.model")]
|
||||||
|
self.assertIn("env.cr.commit()", prix)
|
||||||
|
reco = vif[vif.index("_get_account_reconcile_model") :]
|
||||||
|
self.assertIn("env.cr.commit()", reco)
|
||||||
|
|
||||||
|
def test_it_calls_odoo_rather_than_inventing_records(self):
|
||||||
|
vif = conf.build_script(False)
|
||||||
|
self.assertIn("_activate_or_create_pricelists", vif)
|
||||||
|
self.assertIn("_get_account_reconcile_model", vif)
|
||||||
|
self.assertIn("_load_data", vif)
|
||||||
|
|
||||||
|
def test_it_lets_odoo_skip_what_already_exists(self):
|
||||||
|
# `ignore_duplicates` est ce qui rend l'outil rejouable ; sans
|
||||||
|
# lui, une seconde passe doublerait les modèles.
|
||||||
|
self.assertIn("ignore_duplicates=True", conf.build_script(False))
|
||||||
|
|
||||||
|
def test_it_uses_the_sentinels_run_shell_imposes(self):
|
||||||
|
# En poser d'autres ferait lire un rapport vide.
|
||||||
|
vif = conf.build_script(False)
|
||||||
|
self.assertIn(conf.DEBUT, vif)
|
||||||
|
self.assertIn(conf.FIN, vif)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in a new issue