From d2f70f653501ec34040565824a283bd25dbadb39 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Fri, 21 Aug 2026 03:31:28 -0400 Subject: [PATCH] [ADD] migration quality: explain ir_property and tracking values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- script/analyse/check_migration_quality.py | 38 +++++++++++++- script/todo/todo_i18n.py | 10 ++-- test/test_check_migration_quality.py | 61 ++++++++++++++++++++++- 3 files changed, 103 insertions(+), 6 deletions(-) diff --git a/script/analyse/check_migration_quality.py b/script/analyse/check_migration_quality.py index 2950fc8..f69f3c7 100755 --- a/script/analyse/check_migration_quality.py +++ b/script/analyse/check_migration_quality.py @@ -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}") diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 0706323..2ac36e4 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -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", diff --git a/test/test_check_migration_quality.py b/test/test_check_migration_quality.py index 595395f..0cf4dba 100644 --- a/test/test_check_migration_quality.py +++ b/test/test_check_migration_quality.py @@ -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