[ADD] migration quality: explain ir_property and tracking values

ir_property never empties, it GROWS — 211 rows in 12, 457 in 17 — then
vanishes in 18. Its fields are jsonb columns there, verified on
res_partner.property_payment_term_id.

mail_tracking_value is a REAL loss, not a rename: `field` goes from
varchar to a foreign key in 14, and what would not resolve was deleted.
1528 of the 2336 belonged to the removed agile module; 706 more survived
under a new name. New kind "pruned" so the report cannot call a living
table retired.

--- FR ---

ir_property ne se vide jamais, elle GROSSIT — 211 lignes en 12, 457 en
17 — puis disparaît en 18. Ses champs y sont des colonnes jsonb, vérifié
sur res_partner.property_payment_term_id.

mail_tracking_value est une VRAIE perte, pas un renommage : `field`
passe de varchar à clé étrangère en 14, et ce qui ne se résolvait pas a
été supprimé. 1528 des 2336 venaient du module agile retiré ; 706 autres
ont survécu sous un autre nom. Nouveau genre « pruned » pour que le
rapport ne déclare pas retirée une table bien vivante.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-21 03:31:28 -04:00
parent 4103e13800
commit d2f70f6535
3 changed files with 103 additions and 6 deletions

View file

@ -471,6 +471,18 @@ SEMANTIC_MAP = (
"kind": "retired",
"why": "translations moved into jsonb columns",
},
# Vérifiée palier par palier : la table ne se vide jamais, elle GROSSIT
# (211 en 12 → 457 en 17) puis disparaît d'un coup en 18. Les champs
# qu'elle portait sont des colonnes jsonb en 18 — vérifié sur
# res_partner.property_payment_term_id et
# product_template.property_account_income_id.
{
"since": 18,
"table": "ir_property",
"into": None,
"kind": "retired",
"why": "company-dependent values moved into jsonb columns",
},
{
"since": 17,
"table": "account_tax_template",
@ -537,6 +549,23 @@ SEMANTIC_MAP = (
"kind": "merged",
"why": "inventory adjustments became quants",
},
# Élagages : la table demeure, ces lignes non. À distinguer d'une
# fusion — ici les enregistrements ne continuent NULLE PART.
#
# Vérifié : en 13 `field` est un varchar (le nom du champ), en 14 c'est
# une clé étrangère vers ir_model_fields. En 14 aucune ligne n'a de
# champ nul ni de clé cassée : ce qui ne se résolvait pas a été
# supprimé. Sur les 2336 disparues, 1528 portaient des champs du module
# agile retiré (priority_id 761, agile_enabled 711, story_points 54,
# resolution_id 2) ; 706 autres ont SURVÉCU sous un nouveau nom
# (account.move.type → move_type, 651).
{
"since": 14,
"table": "mail_tracking_value",
"into": None,
"kind": "pruned",
"why": "tracking of fields that ceased to exist",
},
)
@ -812,7 +841,7 @@ def render_compare(diff, colour, limit=8):
if connues:
lignes.append(
f" {paint('▽', 'dim', colour)} {len(connues)}"
f" {t('table(s) Odoo moved or retired')} :"
f" {t('table(s) explained by an Odoo change')} :"
)
for table, avant, apres, connu in connues[:limit]:
if connu["into"]:
@ -826,6 +855,13 @@ def render_compare(diff, colour, limit=8):
else paint(t("but it gained nothing"), "fail", colour)
)
ou = f"→ {connu['into']} ({accueil})"
elif connu["kind"] == "pruned":
# La table est TOUJOURS là : dire « retirée » serait
# faux, et cette perte-ci est réelle — les lignes ne
# continuent nulle part. Le mot doit le laisser voir.
ou = paint(
t("rows dropped, the table remains"), "warn", colour
)
else:
ou = t("retired from the database")
lignes.append(f" {table:<40} {avant:>8} → {apres} {ou}")

View file

@ -5787,9 +5787,13 @@ TRANSLATIONS = {
"fr": "table(s) ont perdu des lignes, sans explication",
"en": "table(s) lost rows, unexplained",
},
"table(s) Odoo moved or retired": {
"fr": "table(s) qu'Odoo a déplacées ou retirées",
"en": "table(s) Odoo moved or retired",
"table(s) explained by an Odoo change": {
"fr": "table(s) expliquées par un changement d'Odoo",
"en": "table(s) explained by an Odoo change",
},
"rows dropped, the table remains": {
"fr": "lignes supprimées, la table demeure",
"en": "rows dropped, the table remains",
},
"retired from the database": {
"fr": "retirée de la base",

View file

@ -331,7 +331,10 @@ class TestTheSemanticMap(Base):
snapshot(odoo="18.0", table={}),
)
texte = "\n".join(quality.render_compare(diff, colour=False))
self.assertLess(texte.index("unexplained"), texte.index("moved or"))
self.assertLess(
texte.index(todo_i18n.t("table(s) lost rows, unexplained")),
texte.index(todo_i18n.t("table(s) explained by an Odoo change")),
)
def test_needaction_became_notifications_in_15(self):
"""Vérifiée palier par palier, pas déduite d'un nom qui se ressemble.
@ -376,7 +379,61 @@ class TestTheSemanticMap(Base):
for cle in ("since", "table", "into", "kind", "why"):
self.assertIn(cle, entree, entree)
self.assertTrue(entree["why"], entree)
self.assertIn(entree["kind"], ("merged", "renamed", "retired"))
self.assertIn(
entree["kind"], ("merged", "renamed", "retired", "pruned")
)
def test_a_pruned_entry_never_claims_a_destination(self):
# « pruned » dit que les lignes ne continuent NULLE PART. Lui donner
# un « into » ferait passer une perte réelle pour une fusion.
for entree in quality.SEMANTIC_MAP:
if entree["kind"] == "pruned":
self.assertIsNone(entree["into"], entree)
def test_properties_became_jsonb_columns_in_18(self):
"""La table ne se vide pas : elle GROSSIT, puis disparaît d'un coup.
211 lignes en 12, 457 en 17, table absente en 18. Les champs
qu'elle portait sont des colonnes jsonb en 18 — vérifié sur
res_partner.property_payment_term_id.
"""
diff = self.perte("ir_property", 457, 0, version="18.0")
connu = diff["rows_lost"][0][3]
self.assertIsNotNone(connu)
self.assertEqual(connu["kind"], "retired")
self.assertIsNone(connu["into"])
def test_properties_are_not_explained_at_the_17_bump(self):
# En 17 la table est à son maximum (457). Dater l'entrée plus tôt
# ferait passer pour attendue une perte qui ne l'est pas.
diff = self.perte("ir_property", 273, 100, version="17.0")
self.assertIsNone(diff["rows_lost"][0][3])
def test_tracking_values_were_pruned_in_14(self):
"""Le champ suivi passe de varchar à clé étrangère au palier 14.
En 14 aucune ligne n'a de clé nulle ni cassée : ce qui ne se
résolvait pas a été supprimé. La table SURVIT — 13833 lignes — donc
l'explication ne doit pas prétendre qu'elle a disparu.
"""
diff = self.perte("mail_tracking_value", 16169, 13833, version="14.0")
table, avant, apres, connu = diff["rows_lost"][0]
self.assertEqual((avant, apres), (16169, 13833))
self.assertIsNotNone(connu)
self.assertEqual(connu["kind"], "pruned")
def test_tracking_values_are_not_explained_at_the_13_bump(self):
# 16167 en 12, 16169 en 13 : rien n'a encore été élagué.
diff = self.perte("mail_tracking_value", 16167, 10000, version="13.0")
self.assertIsNone(diff["rows_lost"][0][3])
def test_a_pruned_loss_is_not_rendered_as_retired(self):
# « retirée de la base » serait faux : la table est toujours là.
diff = self.perte("mail_tracking_value", 16169, 13833, version="14.0")
texte = "\n".join(quality.render_compare(diff, False, 8))
self.assertIn("mail_tracking_value", texte)
self.assertNotIn(todo_i18n.t("retired from the database"), texte)
self.assertIn(todo_i18n.t("rows dropped, the table remains"), texte)
def test_the_column_counts_only_what_needs_an_answer(self):
# Afficher 81 quand 14 sont des refontes voulues ferait fuir le