[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
|
|
|
|
"""Ce qu'une migration a laissé derrière elle, lu dans UNE seule base.
|
|
|
|
|
|
|
|
|
|
`check_migration_quality` répond à une autre question : il compare les
|
|
|
|
|
bases de PALIER qu'une migration locale a laissées, et il lui faut le
|
|
|
|
|
journal de progression. Devant la sauvegarde d'un client, ni l'une ni
|
|
|
|
|
l'autre n'existe. Il fallait donc un outil qui n'ait besoin que de la
|
|
|
|
|
base qu'on a sous la main.
|
|
|
|
|
|
|
|
|
|
Le piège, et pourquoi la moitié des contrôles évidents ont été écartés
|
|
|
|
|
--------------------------------------------------------------------
|
|
|
|
|
Un compteur absolu ne prouve rien. Mesuré sur une chaîne 12 → 18 réelle,
|
|
|
|
|
en comparant la base d'ORIGINE à la migrée :
|
|
|
|
|
|
|
|
|
|
champs stockés sans colonne 25 → 72 ← 25 AVANT toute migration
|
|
|
|
|
modèles sans table 90 → 158 ← 90 AVANT
|
|
|
|
|
contraintes orphelines 155 → 530 ← 155 AVANT
|
|
|
|
|
|
|
|
|
|
Ces trois-là paraissent accablants et ne le sont pas : un modèle abstrait
|
|
|
|
|
n'a jamais de table, un champ hérité n'a jamais sa colonne à lui. Affichés
|
|
|
|
|
bruts, ils font peur pour rien — et un rapport qui fait peur pour rien
|
|
|
|
|
finit par être ignoré en entier.
|
|
|
|
|
|
|
|
|
|
Ne restent ici que les constats qui se jugent SANS point de comparaison,
|
|
|
|
|
parce qu'ils sont faux en eux-mêmes. Les mêmes bases, mêmes mesures :
|
|
|
|
|
|
|
|
|
|
ir_model_relation sans table 0 → 68 la table m2m est nommée,
|
|
|
|
|
elle n'existe pas
|
|
|
|
|
index doublés convention 17 0 → 414 Odoo 17 renomme, sans
|
|
|
|
|
supprimer l'ancien
|
|
|
|
|
liste de prix par défaut absente 0 → 1 `product` installé, son
|
|
|
|
|
xmlid pas là
|
|
|
|
|
|
[REM] résidus : retirer le contrôle res_lang.active à NULL
Le contrôle jugeait cassée une langue dont `active` est NULL : listée
nulle part, plus réactivable. Faux dans la source 18 — un domaine
('active','=',False) compile en (IS NULL OR = FALSE) (models.py:3217),
l'action du menu Langues porte active_test: False, le tri passe par
COALESCE(active, FALSE) et la lecture rend bool(value).
Odoo écrit ce NULL : active = fields.Boolean() sans défaut
(res_lang.py:64) et un res.lang.csv sans la colonne, soit un NULL par
langue ajoutée au catalogue. « Zéro avant, non nul après » ne suffit
donc pas. Un test refuse désormais une clé que plus rien ne définit.
--- EN ---
The check called a language broken when `active` is NULL: listed
nowhere, no longer re-enablable. False in the 18 source — an
('active','=',False) domain compiles to (IS NULL OR = FALSE)
(models.py:3217), the Languages menu action carries active_test: False,
sorting goes through COALESCE(active, FALSE), reading returns bool(value).
Odoo writes that NULL itself: active = fields.Boolean() with no default
(res_lang.py:64) and a res.lang.csv without the column, one NULL per
language added to the catalogue. So "zero before, nonzero after" is not
enough. A test now refuses a key that nothing defines any more.
Assisted-by: Claude Opus 5
(cherry picked from commit ee932334cfde63c1af831316549a432c3d382721)
2026-08-27 02:15:30 -04:00
|
|
|
Zéro avant, non nul après : aucun de ceux-là ne peut s'expliquer
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
autrement que par la migration.
|
|
|
|
|
|
[REM] résidus : retirer le contrôle res_lang.active à NULL
Le contrôle jugeait cassée une langue dont `active` est NULL : listée
nulle part, plus réactivable. Faux dans la source 18 — un domaine
('active','=',False) compile en (IS NULL OR = FALSE) (models.py:3217),
l'action du menu Langues porte active_test: False, le tri passe par
COALESCE(active, FALSE) et la lecture rend bool(value).
Odoo écrit ce NULL : active = fields.Boolean() sans défaut
(res_lang.py:64) et un res.lang.csv sans la colonne, soit un NULL par
langue ajoutée au catalogue. « Zéro avant, non nul après » ne suffit
donc pas. Un test refuse désormais une clé que plus rien ne définit.
--- EN ---
The check called a language broken when `active` is NULL: listed
nowhere, no longer re-enablable. False in the 18 source — an
('active','=',False) domain compiles to (IS NULL OR = FALSE)
(models.py:3217), the Languages menu action carries active_test: False,
sorting goes through COALESCE(active, FALSE), reading returns bool(value).
Odoo writes that NULL itself: active = fields.Boolean() with no default
(res_lang.py:64) and a res.lang.csv without the column, one NULL per
language added to the catalogue. So "zero before, nonzero after" is not
enough. A test now refuses a key that nothing defines any more.
Assisted-by: Claude Opus 5
(cherry picked from commit ee932334cfde63c1af831316549a432c3d382721)
2026-08-27 02:15:30 -04:00
|
|
|
« Zéro avant, non nul après » ne suffit pourtant pas
|
|
|
|
|
----------------------------------------------------
|
|
|
|
|
Un quatrième contrôle a vécu ici et n'y est plus : `res_lang.active` à
|
|
|
|
|
NULL, 0 avant et 9 après. Le chiffre était juste, la conclusion fausse.
|
|
|
|
|
|
|
|
|
|
Mesuré palier par palier : 0, 1, 2, 3, 5, 8, 9 — les NULL arrivent avec
|
|
|
|
|
les langues que CHAQUE version ajoute au catalogue. Et c'est Odoo
|
|
|
|
|
lui-même qui les écrit : `active = fields.Boolean()` sans défaut
|
|
|
|
|
(res_lang.py:64) et un `res.lang.csv` sans colonne `active` — l'INSERT ne
|
|
|
|
|
porte pas la colonne, PostgreSQL y met NULL.
|
|
|
|
|
|
|
|
|
|
Aucune conséquence, vérifiée dans la source de la 18 : un domaine
|
|
|
|
|
`('active','=',False)` compile en `(IS NULL OR = FALSE)`
|
|
|
|
|
(models.py:3217-3222), l'action du menu Langues porte `active_test: False`
|
|
|
|
|
(res_lang_views.xml:136), le tri passe par `COALESCE(active, FALSE)`
|
|
|
|
|
(models.py:5692) et la lecture rend `bool(value)` (fields.py:1515). NULL
|
|
|
|
|
et FALSE sont indiscernables partout.
|
|
|
|
|
|
|
|
|
|
La leçon : la croissance mesurée doit AUSSI être inexplicable autrement.
|
|
|
|
|
Ici elle s'expliquait très bien.
|
|
|
|
|
|
[FIX] analyse: retirer un constat qui faisait peur pour rien
« ir_model_relation nomme une table absente » : 0 avant la migration, 68
après. Le profil idéal — et aucune conséquence.
Son unique consommateur, _module_data_uninstall dans
base/models/ir_model.py, teste sql.table_exists() AVANT de supprimer : la
ligne périmée est ignorée, puis effacée. J'écrivais « la prochaine mise à
jour de module tente de la modifier et échoue » : c'est faux.
Les 68 appartiennent de plus à des modules INSTALLÉS, et database_cleanup
ne touche que les désinstallés — la réparation désignée n'en aurait
réparé aucune.
Un constat sans conséquence et sans geste possible est du bruit, quelle
que soit la netteté du signal. Restent trois constats, tous réels.
--- EN ---
« ir_model_relation names a missing table »: 0 before the migration, 68
after. The ideal profile — and no consequence whatsoever.
Its only consumer, _module_data_uninstall in base/models/ir_model.py,
tests sql.table_exists() BEFORE dropping: the stale row is skipped, then
unlinked. I wrote "the next module update tries to alter it and fails":
that is false.
The 68 also belong to INSTALLED modules, and database_cleanup only
touches uninstalled ones — the repair I named would have fixed none.
A finding with no consequence and no possible action is noise, however
clean the signal. Three findings remain, all real.
Assisted-by: Claude Opus 5
2026-08-24 23:28:23 -04:00
|
|
|
Un quatrième a été RETIRÉ après vérification
|
|
|
|
|
--------------------------------------------
|
|
|
|
|
« ir_model_relation nomme une table absente » : 0 avant, 68 après, le
|
|
|
|
|
profil idéal. Et sans la moindre conséquence. Son unique consommateur,
|
|
|
|
|
`_module_data_uninstall` dans `base/models/ir_model.py`, teste
|
|
|
|
|
`sql.table_exists(...)` AVANT de supprimer : la ligne périmée est
|
|
|
|
|
ignorée, puis effacée. Les 68 appartiennent en outre à des modules
|
|
|
|
|
INSTALLÉS, que `database_cleanup` ne touche pas — la réparation que ce
|
|
|
|
|
fichier désignait n'en aurait réparé aucune.
|
|
|
|
|
|
|
|
|
|
Un constat sans conséquence et sans geste possible est du bruit, quelle
|
|
|
|
|
que soit la netteté du signal. Il est parti.
|
|
|
|
|
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
Chaque constat nomme l'outil qui le répare. Un rapport qui montre un
|
|
|
|
|
dégât sans dire quoi lancer oblige à chercher, et on ne cherche pas.
|
|
|
|
|
|
|
|
|
|
Lecture seule : `default_transaction_read_only=on`, imposé par le serveur.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.path.append(
|
|
|
|
|
os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from script.todo.todo_i18n import t
|
|
|
|
|
except Exception: # pragma: no cover - repli si i18n indisponible
|
|
|
|
|
|
|
|
|
|
def t(key: str) -> str:
|
|
|
|
|
return key
|
|
|
|
|
|
|
|
|
|
|
[ADD] qualité de migration : verdicts, sources, revue
Le rapport comparait les paliers sans dire si la migration avait réussi,
alors que les verdicts dorment déjà dans lst_event du journal de
progression : des contrôles en échec y restent sans remonter nulle part.
Trois sections s'ajoutent sous les paliers : les verdicts, rattachés au
palier ODOO et non au compteur du pilote, décalé d'un rang ; où vivent les
traces, car config.conf laisse logfile= vide et la sortie d'Odoo meurt avec
le terminal ; et la revue, six étapes lançables par « r ». Le contrôle de
résidus porte la même section sans toucher son code de sortie : un verdict
vient du fichier, pas de la base.
--- EN ---
The report compared the tiers without saying whether the migration had
succeeded, while the verdicts already sit in lst_event of the progression
file: failed checks stay there and surface nowhere.
Three sections are added below the tiers: the verdicts, tied to the ODOO
tier and not to the driver counter, which is off by one; where the traces
live, since config.conf leaves logfile= empty and Odoo's output dies with
the terminal; and the review, six steps runnable with "r". The residue
check carries the same section without touching its exit code: a verdict
comes from the file, not from the database.
Assisted-by: Claude Opus 5
(cherry picked from commit b05e0333c4b94d58eb794f09a2459e41d826c655)
2026-08-26 07:46:32 -04:00
|
|
|
from script.analyse import check_migration_quality as quality # noqa: E402
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
from script.analyse import lib_analyse # noqa: E402
|
|
|
|
|
|
|
|
|
|
COULEURS = {
|
|
|
|
|
"broken": "\033[31m",
|
[ADD] qualité de migration : verdicts, sources, revue
Le rapport comparait les paliers sans dire si la migration avait réussi,
alors que les verdicts dorment déjà dans lst_event du journal de
progression : des contrôles en échec y restent sans remonter nulle part.
Trois sections s'ajoutent sous les paliers : les verdicts, rattachés au
palier ODOO et non au compteur du pilote, décalé d'un rang ; où vivent les
traces, car config.conf laisse logfile= vide et la sortie d'Odoo meurt avec
le terminal ; et la revue, six étapes lançables par « r ». Le contrôle de
résidus porte la même section sans toucher son code de sortie : un verdict
vient du fichier, pas de la base.
--- EN ---
The report compared the tiers without saying whether the migration had
succeeded, while the verdicts already sit in lst_event of the progression
file: failed checks stay there and surface nowhere.
Three sections are added below the tiers: the verdicts, tied to the ODOO
tier and not to the driver counter, which is off by one; where the traces
live, since config.conf leaves logfile= empty and Odoo's output dies with
the terminal; and the review, six steps runnable with "r". The residue
check carries the same section without touching its exit code: a verdict
comes from the file, not from the database.
Assisted-by: Claude Opus 5
(cherry picked from commit b05e0333c4b94d58eb794f09a2459e41d826c655)
2026-08-26 07:46:32 -04:00
|
|
|
"step": "\033[36m",
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
"watch": "\033[33m",
|
|
|
|
|
"ok": "\033[32m",
|
|
|
|
|
"dim": "\033[90m",
|
|
|
|
|
}
|
|
|
|
|
RESET = "\033[0m"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def paint(texte, genre, colour):
|
|
|
|
|
"""Teinter, ou rendre le texte tel quel quand la couleur est coupée."""
|
|
|
|
|
if not colour:
|
|
|
|
|
return texte
|
|
|
|
|
return f"{COULEURS.get(genre, '')}{texte}{RESET}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# `sql` doit rendre UN nombre. `repair` nomme l'outil qui corrige, ou None
|
|
|
|
|
# quand il n'y en a pas encore — le dire vaut mieux que de laisser croire.
|
|
|
|
|
CONTROLES = (
|
|
|
|
|
{
|
|
|
|
|
"key": "stuck_modules",
|
|
|
|
|
"title": "Modules stuck between two states",
|
|
|
|
|
"why": "A migration that stopped mid-flight leaves them there;"
|
|
|
|
|
" Odoo will retry the transition at every start.",
|
|
|
|
|
"sql": "SELECT count(*) FROM ir_module_module"
|
|
|
|
|
" WHERE state IN ('to install','to upgrade','to remove')",
|
|
|
|
|
"gravity": "broken",
|
|
|
|
|
"repair": "script/todo/todo_upgrade.py (uninstall_one_by_one)",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"key": "duplicate_index",
|
|
|
|
|
"title": "Indexes duplicated by the Odoo 17 renaming",
|
|
|
|
|
"why": "Odoo 17 changed the naming convention without dropping the"
|
[ADD] migration: brancher les deux réparations qui ne tournaient jamais
fix_duplicate_index et restore_config_defaults étaient écrits, éprouvés,
et absents du pilote. Chaque migration refabriquait donc ses index
redondants et reperdait sa liste de prix.
Mesuré sur DEUX chaînes 12 → 18 indépendantes, même base source : 68
relations orphelines, 9 langues au drapeau NULL, une liste de prix
absente, 376 paires d'index en double — les mêmes nombres des deux côtés.
Ce n'est pas un accident d'exécution, c'est le chemin lui-même.
Les index à partir du palier 17 : avant, la convention n'a pas changé.
Les réglages au DERNIER palier : l'outil charge le registre, et seul
l'état final compte. Les deux en wait_at_error=False — avec --apply, le
code 1 dit « il en reste », pas « je suis tombé », et cela n'arrête pas
six paliers.
--- EN ---
fix_duplicate_index and restore_config_defaults were written, proven, and
absent from the driver. Every migration therefore rebuilt its redundant
indexes and lost its default pricelist again.
Measured on TWO independent 12 → 18 chains from the same source: 68
orphan relations, 9 languages with a NULL flag, one missing pricelist,
376 duplicate index pairs — the same numbers on both. Not a fluke of one
run: the path itself.
Indexes from step 17 onward: before that the convention had not changed.
Config defaults at the LAST step: the tool loads the registry, and only
the final state matters. Both with wait_at_error=False — with --apply,
exit 1 means "some remain", not "I crashed", and that must not halt six
steps.
Assisted-by: Claude Opus 5
2026-08-24 22:55:21 -04:00
|
|
|
" old index: both are maintained on every write. This count is a"
|
|
|
|
|
" cheap signal — the repair tool compares columns and"
|
|
|
|
|
" uniqueness, and is the one to trust.",
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
"sql": "SELECT count(*) FROM pg_indexes a WHERE a.schemaname='public'"
|
|
|
|
|
" AND a.indexname ~ '__[a-z0-9_]+_index$'"
|
|
|
|
|
" AND EXISTS (SELECT 1 FROM pg_indexes b WHERE b.schemaname='public'"
|
|
|
|
|
" AND b.tablename = a.tablename"
|
|
|
|
|
" AND b.indexname = replace(a.indexname, '__', '_'))",
|
|
|
|
|
"gravity": "watch",
|
|
|
|
|
"repair": "script/odoo/migration/fix_duplicate_index.py --apply",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"key": "missing_pricelist",
|
|
|
|
|
"title": "Default pricelist missing while product is installed",
|
|
|
|
|
"why": "product.list0 was declared up to Odoo 16 only; nothing"
|
|
|
|
|
" recreates it, and a quotation has no price list to pick.",
|
2026-08-25 00:58:14 -04:00
|
|
|
# On cherche une LISTE, pas son xmlid. Mesuré : la réparation
|
|
|
|
|
# laisse Odoo créer « Par défaut » sans poser `product.list0` —
|
|
|
|
|
# chercher l'xmlid signalait donc une base parfaitement saine, et
|
|
|
|
|
# aurait signalé de même celle d'un client qui a créé la sienne à
|
|
|
|
|
# la main.
|
[FIX] liste de prix : décider sur la fonctionnalité, pas sur l'exécutant
`env.user.has_group()` répond « oui » dès que l'exécutant est membre du
groupe, et la migration l'y ajoute en cours de route. Or la case des
réglages lit tout autre chose : ce que `base.group_user` IMPLIQUE
(res_config.py, « which groups are implied by the group Employee »).
Décider sur l'exécutant créait une liste de prix dans une base dont la
fonctionnalité est éteinte, et Odoo prévenait à chaque ouverture des
réglages qu'il allait l'archiver. Le contrôle « restant de migration »
posait la même mauvaise question ; les deux lisent désormais l'implication
du groupe. Vérifié sur copie jetable, dans les deux sens : fonctionnalité
éteinte, rien n'est signalé ; activée, constat et réparation reviennent.
--- EN ---
`env.user.has_group()` says yes as soon as the caller belongs to the
group, and the migration adds it along the way. But the settings checkbox
reads something else: what `base.group_user` IMPLIES (res_config.py,
"which groups are implied by the group Employee"). Deciding on the caller
created a pricelist in a database whose feature is off, and Odoo warned on
every opening of the settings that it would archive it. The
migration-residue check asked the same wrong question; both now read the
group implication. Verified on a throwaway copy, both ways: feature off,
nothing is reported; feature on, finding and repair come back.
Assisted-by: Claude Opus 5
(cherry picked from commit 38ba25e01894f45fb956bcbb08cc3d96a125e648)
2026-08-26 03:58:45 -04:00
|
|
|
#
|
|
|
|
|
# Et seulement si la FONCTIONNALITÉ est active, c'est-à-dire si
|
|
|
|
|
# `base.group_user` implique `product.group_product_pricelist` —
|
|
|
|
|
# la question exacte que pose la case des réglages. Sans elle,
|
|
|
|
|
# l'absence de liste est normale ; signaler quand même menait à
|
|
|
|
|
# créer une liste dans une base qui n'en veut pas, et Odoo
|
|
|
|
|
# prévenait alors à chaque ouverture des réglages qu'il allait
|
|
|
|
|
# l'archiver.
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
"sql": "SELECT CASE WHEN EXISTS (SELECT 1 FROM ir_module_module"
|
|
|
|
|
" WHERE name='product' AND state='installed')"
|
2026-08-25 00:58:14 -04:00
|
|
|
" AND to_regclass('public.product_pricelist') IS NOT NULL"
|
[FIX] liste de prix : décider sur la fonctionnalité, pas sur l'exécutant
`env.user.has_group()` répond « oui » dès que l'exécutant est membre du
groupe, et la migration l'y ajoute en cours de route. Or la case des
réglages lit tout autre chose : ce que `base.group_user` IMPLIQUE
(res_config.py, « which groups are implied by the group Employee »).
Décider sur l'exécutant créait une liste de prix dans une base dont la
fonctionnalité est éteinte, et Odoo prévenait à chaque ouverture des
réglages qu'il allait l'archiver. Le contrôle « restant de migration »
posait la même mauvaise question ; les deux lisent désormais l'implication
du groupe. Vérifié sur copie jetable, dans les deux sens : fonctionnalité
éteinte, rien n'est signalé ; activée, constat et réparation reviennent.
--- EN ---
`env.user.has_group()` says yes as soon as the caller belongs to the
group, and the migration adds it along the way. But the settings checkbox
reads something else: what `base.group_user` IMPLIES (res_config.py,
"which groups are implied by the group Employee"). Deciding on the caller
created a pricelist in a database whose feature is off, and Odoo warned on
every opening of the settings that it would archive it. The
migration-residue check asked the same wrong question; both now read the
group implication. Verified on a throwaway copy, both ways: feature off,
nothing is reported; feature on, finding and repair come back.
Assisted-by: Claude Opus 5
(cherry picked from commit 38ba25e01894f45fb956bcbb08cc3d96a125e648)
2026-08-26 03:58:45 -04:00
|
|
|
" AND EXISTS (SELECT 1 FROM res_groups_implied_rel r"
|
|
|
|
|
" JOIN ir_model_data u ON u.model='res.groups' AND u.res_id=r.gid"
|
|
|
|
|
" AND u.module='base' AND u.name='group_user'"
|
|
|
|
|
" JOIN ir_model_data g ON g.model='res.groups' AND g.res_id=r.hid"
|
|
|
|
|
" AND g.module='product' AND g.name='group_product_pricelist')"
|
2026-08-25 00:58:14 -04:00
|
|
|
" AND NOT EXISTS (SELECT 1 FROM product_pricelist)"
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
" THEN 1 ELSE 0 END",
|
|
|
|
|
"gravity": "broken",
|
|
|
|
|
"repair": "script/odoo/migration/restore_config_defaults.py --apply",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"key": "view_model_gone",
|
|
|
|
|
"title": "Views bound to a model that no longer exists",
|
|
|
|
|
"why": "Opening one raises; the menu that leads to it is a dead end.",
|
|
|
|
|
"sql": "SELECT count(*) FROM ir_ui_view v"
|
|
|
|
|
" WHERE v.model IS NOT NULL AND v.model <> ''"
|
|
|
|
|
" AND NOT EXISTS (SELECT 1 FROM ir_model m WHERE m.model = v.model)",
|
|
|
|
|
"gravity": "broken",
|
|
|
|
|
"repair": None,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"key": "view_parent_gone",
|
|
|
|
|
"title": "Views inheriting a view that is gone",
|
|
|
|
|
"why": "The whole inheritance chain below them stops rendering.",
|
|
|
|
|
"sql": "SELECT count(*) FROM ir_ui_view v WHERE v.inherit_id IS NOT"
|
|
|
|
|
" NULL AND NOT EXISTS"
|
|
|
|
|
" (SELECT 1 FROM ir_ui_view p WHERE p.id = v.inherit_id)",
|
|
|
|
|
"gravity": "broken",
|
|
|
|
|
"repair": "script/odoo/migration/fix_cow_render.py --apply",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"key": "xmlid_model_gone",
|
|
|
|
|
"title": "External ids pointing at a model that is gone",
|
|
|
|
|
"why": "Every module update that resolves one of them fails.",
|
|
|
|
|
"sql": "SELECT count(*) FROM ir_model_data d WHERE NOT EXISTS"
|
|
|
|
|
" (SELECT 1 FROM ir_model m WHERE m.model = d.model)",
|
|
|
|
|
"gravity": "broken",
|
[REM] résidus : retirer le contrôle res_lang.active à NULL
Le contrôle jugeait cassée une langue dont `active` est NULL : listée
nulle part, plus réactivable. Faux dans la source 18 — un domaine
('active','=',False) compile en (IS NULL OR = FALSE) (models.py:3217),
l'action du menu Langues porte active_test: False, le tri passe par
COALESCE(active, FALSE) et la lecture rend bool(value).
Odoo écrit ce NULL : active = fields.Boolean() sans défaut
(res_lang.py:64) et un res.lang.csv sans la colonne, soit un NULL par
langue ajoutée au catalogue. « Zéro avant, non nul après » ne suffit
donc pas. Un test refuse désormais une clé que plus rien ne définit.
--- EN ---
The check called a language broken when `active` is NULL: listed
nowhere, no longer re-enablable. False in the 18 source — an
('active','=',False) domain compiles to (IS NULL OR = FALSE)
(models.py:3217), the Languages menu action carries active_test: False,
sorting goes through COALESCE(active, FALSE), reading returns bool(value).
Odoo writes that NULL itself: active = fields.Boolean() with no default
(res_lang.py:64) and a res.lang.csv without the column, one NULL per
language added to the catalogue. So "zero before, nonzero after" is not
enough. A test now refuses a key that nothing defines any more.
Assisted-by: Claude Opus 5
(cherry picked from commit ee932334cfde63c1af831316549a432c3d382721)
2026-08-27 02:15:30 -04:00
|
|
|
"repair": "script/odoo/migration/database_cleanup.py",
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"key": "attachment_field_gone",
|
|
|
|
|
"title": "Attachments whose carrying field is gone",
|
|
|
|
|
"why": "Odoo raises a KeyError merely checking them, and nothing"
|
|
|
|
|
" will ever read them again.",
|
|
|
|
|
"sql": "SELECT count(*) FROM ir_attachment a"
|
|
|
|
|
" WHERE a.res_field IS NOT NULL AND a.res_field <> ''"
|
|
|
|
|
" AND NOT EXISTS (SELECT 1 FROM ir_model_fields f"
|
|
|
|
|
" WHERE f.model = a.res_model AND f.name = a.res_field)",
|
|
|
|
|
"gravity": "watch",
|
|
|
|
|
"repair": "script/analyse/check_filestore.py",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def inspect(database, config_path=None):
|
|
|
|
|
"""Passer chaque contrôle, et rendre son nombre.
|
|
|
|
|
|
|
|
|
|
Un contrôle qui ÉCHOUE n'est pas un contrôle qui rend zéro : une table
|
|
|
|
|
absente parce que le module n'est pas installé n'est pas un dégât. On
|
|
|
|
|
garde l'erreur telle quelle, et le rendu la distingue.
|
|
|
|
|
"""
|
|
|
|
|
resultats = {}
|
|
|
|
|
for controle in CONTROLES:
|
|
|
|
|
try:
|
|
|
|
|
brut = lib_analyse.run_psql(
|
|
|
|
|
database, controle["sql"], config_path=config_path
|
|
|
|
|
).strip()
|
|
|
|
|
resultats[controle["key"]] = int(brut.splitlines()[0])
|
|
|
|
|
except Exception as exc: # noqa: BLE001 - on rapporte, on ne meurt pas
|
|
|
|
|
resultats[controle["key"]] = {
|
|
|
|
|
"error": str(exc).splitlines()[0][:120]
|
|
|
|
|
}
|
|
|
|
|
return resultats
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def judge(resultats):
|
|
|
|
|
"""Ne garder que ce qui compte, le plus grave d'abord.
|
|
|
|
|
|
|
|
|
|
Un contrôle illisible remonte AVEC les autres : ne pas avoir pu
|
|
|
|
|
regarder n'est pas la même chose que n'avoir rien trouvé, et taire la
|
|
|
|
|
différence est exactement ce qui fait prendre un rapport pour une
|
|
|
|
|
garantie.
|
|
|
|
|
"""
|
|
|
|
|
trouve, illisibles = [], []
|
|
|
|
|
for controle in CONTROLES:
|
|
|
|
|
valeur = resultats.get(controle["key"])
|
|
|
|
|
if isinstance(valeur, dict):
|
|
|
|
|
illisibles.append((controle, valeur.get("error", "")))
|
|
|
|
|
elif valeur:
|
|
|
|
|
trouve.append((controle, valeur))
|
|
|
|
|
trouve.sort(key=lambda pair: (pair[0]["gravity"] != "broken", -pair[1]))
|
|
|
|
|
return trouve, illisibles
|
|
|
|
|
|
|
|
|
|
|
[ADD] qualité de migration : verdicts, sources, revue
Le rapport comparait les paliers sans dire si la migration avait réussi,
alors que les verdicts dorment déjà dans lst_event du journal de
progression : des contrôles en échec y restent sans remonter nulle part.
Trois sections s'ajoutent sous les paliers : les verdicts, rattachés au
palier ODOO et non au compteur du pilote, décalé d'un rang ; où vivent les
traces, car config.conf laisse logfile= vide et la sortie d'Odoo meurt avec
le terminal ; et la revue, six étapes lançables par « r ». Le contrôle de
résidus porte la même section sans toucher son code de sortie : un verdict
vient du fichier, pas de la base.
--- EN ---
The report compared the tiers without saying whether the migration had
succeeded, while the verdicts already sit in lst_event of the progression
file: failed checks stay there and surface nowhere.
Three sections are added below the tiers: the verdicts, tied to the ODOO
tier and not to the driver counter, which is off by one; where the traces
live, since config.conf leaves logfile= empty and Odoo's output dies with
the terminal; and the review, six steps runnable with "r". The residue
check carries the same section without touching its exit code: a verdict
comes from the file, not from the database.
Assisted-by: Claude Opus 5
(cherry picked from commit b05e0333c4b94d58eb794f09a2459e41d826c655)
2026-08-26 07:46:32 -04:00
|
|
|
def famille(nom):
|
|
|
|
|
"""La base d'origine dont ce nom est un palier.
|
|
|
|
|
|
|
|
|
|
« test_neutralize_upgrade_14 » et « …_upgrade_18 » sont deux paliers
|
|
|
|
|
de la MÊME migration. Interroger la base 18 doit montrer l'échec du
|
|
|
|
|
palier 14 : c'est le seul endroit où il subsiste.
|
|
|
|
|
"""
|
|
|
|
|
return nom.rsplit("_upgrade_", 1)[0] if "_upgrade_" in nom else nom
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verdicts(database, path=None):
|
|
|
|
|
"""(tous, ratés) pour cette base — lus dans le FICHIER, pas en SQL.
|
|
|
|
|
|
|
|
|
|
Un test de fumée qui échoue ne laisse aucune trace en base : rien
|
|
|
|
|
n'est écrit, rien n'est cassé, la requête suivante répond. Les
|
|
|
|
|
contrôles ci-dessus sont donc structurellement aveugles à ce type
|
|
|
|
|
d'échec, et c'était la moitié de ce qu'une migration peut rater.
|
|
|
|
|
"""
|
|
|
|
|
dct = quality.read_progression(path or quality.DEFAULT_PROGRESSION)
|
|
|
|
|
lignee = famille(database)
|
|
|
|
|
tous = [
|
|
|
|
|
e
|
|
|
|
|
for e in quality.read_events(dct)
|
|
|
|
|
if famille(quality.event_database(e)) == lignee
|
|
|
|
|
]
|
|
|
|
|
return tous, quality.failures(tous)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verdicts_block(database, colour=True, path=None):
|
|
|
|
|
"""La section « Verdicts », ou rien du tout s'il n'y en a pas.
|
|
|
|
|
|
|
|
|
|
Silencieuse quand le fichier n'existe pas : devant la sauvegarde d'un
|
|
|
|
|
client, il n'y a jamais eu de migration locale, et annoncer l'absence
|
|
|
|
|
d'un fichier qu'on n'attendait pas ne renseigne personne.
|
|
|
|
|
"""
|
|
|
|
|
chemin = path or quality.DEFAULT_PROGRESSION
|
|
|
|
|
tous, ratés = verdicts(database, chemin)
|
|
|
|
|
if not tous:
|
|
|
|
|
return []
|
|
|
|
|
lignes = [
|
|
|
|
|
"",
|
|
|
|
|
paint(f"🚦 {t('Verdicts the migration recorded')}", "step", colour),
|
|
|
|
|
]
|
|
|
|
|
lignes.append(paint(f" {t('recorded in')} {chemin}", "dim", colour))
|
|
|
|
|
lignes.append("")
|
|
|
|
|
if not ratés:
|
|
|
|
|
lignes.append(
|
|
|
|
|
paint(
|
|
|
|
|
f"✅ {str(len(tous)).rjust(6)} {t('checks, all passed')}",
|
|
|
|
|
"ok",
|
|
|
|
|
colour,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
for event in ratés:
|
|
|
|
|
palier = quality.event_step(event)
|
|
|
|
|
lignes.append(
|
|
|
|
|
paint(
|
|
|
|
|
f"❌ {palier.rjust(6)} {event['name']}",
|
|
|
|
|
"broken",
|
|
|
|
|
colour,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
lignes.append(
|
|
|
|
|
paint(f" {event['detail'][:120]}", "dim", colour)
|
|
|
|
|
)
|
|
|
|
|
lignes.append("")
|
|
|
|
|
lignes.append(
|
|
|
|
|
paint(
|
|
|
|
|
f" {t('These come from the file, not the database:')}"
|
|
|
|
|
f" {t('the exit code ignores them.')}",
|
|
|
|
|
"dim",
|
|
|
|
|
colour,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return lignes
|
|
|
|
|
|
|
|
|
|
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
def render(database, resultats, version=None, colour=True):
|
|
|
|
|
"""Le rapport lisible. Chaque constat dit quoi lancer pour le réparer."""
|
|
|
|
|
trouve, illisibles = judge(resultats)
|
|
|
|
|
lignes = [f"🚚 {t('Migration residue in')} {database}"]
|
|
|
|
|
if version:
|
|
|
|
|
lignes.append(f" {t('base module version:')} {version}")
|
|
|
|
|
lignes.append("")
|
|
|
|
|
|
|
|
|
|
if not trouve and not illisibles:
|
|
|
|
|
lignes.append(
|
|
|
|
|
paint(
|
|
|
|
|
f"✅ {t('None of the checks found anything.')}", "ok", colour
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
lignes.append(
|
|
|
|
|
paint(
|
|
|
|
|
f" {t('This reads one database on its own — it cannot see')}"
|
|
|
|
|
f" {t('what an earlier step silently dropped.')}",
|
|
|
|
|
"dim",
|
|
|
|
|
colour,
|
|
|
|
|
)
|
|
|
|
|
)
|
[ADD] qualité de migration : verdicts, sources, revue
Le rapport comparait les paliers sans dire si la migration avait réussi,
alors que les verdicts dorment déjà dans lst_event du journal de
progression : des contrôles en échec y restent sans remonter nulle part.
Trois sections s'ajoutent sous les paliers : les verdicts, rattachés au
palier ODOO et non au compteur du pilote, décalé d'un rang ; où vivent les
traces, car config.conf laisse logfile= vide et la sortie d'Odoo meurt avec
le terminal ; et la revue, six étapes lançables par « r ». Le contrôle de
résidus porte la même section sans toucher son code de sortie : un verdict
vient du fichier, pas de la base.
--- EN ---
The report compared the tiers without saying whether the migration had
succeeded, while the verdicts already sit in lst_event of the progression
file: failed checks stay there and surface nowhere.
Three sections are added below the tiers: the verdicts, tied to the ODOO
tier and not to the driver counter, which is off by one; where the traces
live, since config.conf leaves logfile= empty and Odoo's output dies with
the terminal; and the review, six steps runnable with "r". The residue
check carries the same section without touching its exit code: a verdict
comes from the file, not from the database.
Assisted-by: Claude Opus 5
(cherry picked from commit b05e0333c4b94d58eb794f09a2459e41d826c655)
2026-08-26 07:46:32 -04:00
|
|
|
return "\n".join(lignes + verdicts_block(database, colour)).rstrip()
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
|
|
|
|
for controle, combien in trouve:
|
|
|
|
|
icone = "❌" if controle["gravity"] == "broken" else "⚠"
|
|
|
|
|
lignes.append(
|
|
|
|
|
paint(
|
|
|
|
|
f"{icone} {str(combien).rjust(6)} {t(controle['title'])}",
|
|
|
|
|
controle["gravity"],
|
|
|
|
|
colour,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
lignes.append(paint(f" {t(controle['why'])}", "dim", colour))
|
|
|
|
|
if controle["repair"]:
|
|
|
|
|
lignes.append(f" → {controle['repair']}")
|
|
|
|
|
else:
|
|
|
|
|
lignes.append(
|
|
|
|
|
paint(f" → {t('no repair tool yet')}", "dim", colour)
|
|
|
|
|
)
|
|
|
|
|
lignes.append("")
|
|
|
|
|
|
|
|
|
|
for controle, erreur in illisibles:
|
|
|
|
|
lignes.append(
|
|
|
|
|
paint(
|
|
|
|
|
f"❔ {t(controle['title'])} — {t('could not read')}",
|
|
|
|
|
"watch",
|
|
|
|
|
colour,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
lignes.append(paint(f" {erreur}", "dim", colour))
|
[ADD] qualité de migration : verdicts, sources, revue
Le rapport comparait les paliers sans dire si la migration avait réussi,
alors que les verdicts dorment déjà dans lst_event du journal de
progression : des contrôles en échec y restent sans remonter nulle part.
Trois sections s'ajoutent sous les paliers : les verdicts, rattachés au
palier ODOO et non au compteur du pilote, décalé d'un rang ; où vivent les
traces, car config.conf laisse logfile= vide et la sortie d'Odoo meurt avec
le terminal ; et la revue, six étapes lançables par « r ». Le contrôle de
résidus porte la même section sans toucher son code de sortie : un verdict
vient du fichier, pas de la base.
--- EN ---
The report compared the tiers without saying whether the migration had
succeeded, while the verdicts already sit in lst_event of the progression
file: failed checks stay there and surface nowhere.
Three sections are added below the tiers: the verdicts, tied to the ODOO
tier and not to the driver counter, which is off by one; where the traces
live, since config.conf leaves logfile= empty and Odoo's output dies with
the terminal; and the review, six steps runnable with "r". The residue
check carries the same section without touching its exit code: a verdict
comes from the file, not from the database.
Assisted-by: Claude Opus 5
(cherry picked from commit b05e0333c4b94d58eb794f09a2459e41d826c655)
2026-08-26 07:46:32 -04:00
|
|
|
return "\n".join(lignes + verdicts_block(database, colour)).rstrip()
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv=None):
|
|
|
|
|
import argparse
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
|
description=t("What a migration left behind, read in one database."),
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument("-d", "--database", required=True)
|
|
|
|
|
parser.add_argument("-c", "--config", help="odoo config file")
|
|
|
|
|
parser.add_argument("--json", action="store_true")
|
|
|
|
|
parser.add_argument("--no-color", action="store_true")
|
|
|
|
|
args = parser.parse_args(argv)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
lib_analyse.require_odoo_database(
|
|
|
|
|
args.database, config_path=args.config
|
|
|
|
|
)
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
print(f"❌ {exc}", file=sys.stderr)
|
|
|
|
|
return 2
|
|
|
|
|
|
|
|
|
|
resultats = inspect(args.database, args.config)
|
|
|
|
|
try:
|
|
|
|
|
version = lib_analyse.database_version(
|
|
|
|
|
args.database, config_path=args.config
|
|
|
|
|
)
|
|
|
|
|
except Exception: # noqa: BLE001 - la version est un confort, pas le sujet
|
|
|
|
|
version = None
|
|
|
|
|
trouve, illisibles = judge(resultats)
|
|
|
|
|
|
|
|
|
|
if args.json:
|
[ADD] qualité de migration : verdicts, sources, revue
Le rapport comparait les paliers sans dire si la migration avait réussi,
alors que les verdicts dorment déjà dans lst_event du journal de
progression : des contrôles en échec y restent sans remonter nulle part.
Trois sections s'ajoutent sous les paliers : les verdicts, rattachés au
palier ODOO et non au compteur du pilote, décalé d'un rang ; où vivent les
traces, car config.conf laisse logfile= vide et la sortie d'Odoo meurt avec
le terminal ; et la revue, six étapes lançables par « r ». Le contrôle de
résidus porte la même section sans toucher son code de sortie : un verdict
vient du fichier, pas de la base.
--- EN ---
The report compared the tiers without saying whether the migration had
succeeded, while the verdicts already sit in lst_event of the progression
file: failed checks stay there and surface nowhere.
Three sections are added below the tiers: the verdicts, tied to the ODOO
tier and not to the driver counter, which is off by one; where the traces
live, since config.conf leaves logfile= empty and Odoo's output dies with
the terminal; and the review, six steps runnable with "r". The residue
check carries the same section without touching its exit code: a verdict
comes from the file, not from the database.
Assisted-by: Claude Opus 5
(cherry picked from commit b05e0333c4b94d58eb794f09a2459e41d826c655)
2026-08-26 07:46:32 -04:00
|
|
|
tous_verdicts, verdicts_ratés = verdicts(args.database)
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
print(
|
|
|
|
|
json.dumps(
|
|
|
|
|
{
|
|
|
|
|
"database": args.database,
|
|
|
|
|
"base_version": version,
|
|
|
|
|
"checks": resultats,
|
|
|
|
|
"found": [c["key"] for c, _ in trouve],
|
|
|
|
|
"unreadable": [c["key"] for c, _ in illisibles],
|
[ADD] qualité de migration : verdicts, sources, revue
Le rapport comparait les paliers sans dire si la migration avait réussi,
alors que les verdicts dorment déjà dans lst_event du journal de
progression : des contrôles en échec y restent sans remonter nulle part.
Trois sections s'ajoutent sous les paliers : les verdicts, rattachés au
palier ODOO et non au compteur du pilote, décalé d'un rang ; où vivent les
traces, car config.conf laisse logfile= vide et la sortie d'Odoo meurt avec
le terminal ; et la revue, six étapes lançables par « r ». Le contrôle de
résidus porte la même section sans toucher son code de sortie : un verdict
vient du fichier, pas de la base.
--- EN ---
The report compared the tiers without saying whether the migration had
succeeded, while the verdicts already sit in lst_event of the progression
file: failed checks stay there and surface nowhere.
Three sections are added below the tiers: the verdicts, tied to the ODOO
tier and not to the driver counter, which is off by one; where the traces
live, since config.conf leaves logfile= empty and Odoo's output dies with
the terminal; and the review, six steps runnable with "r". The residue
check carries the same section without touching its exit code: a verdict
comes from the file, not from the database.
Assisted-by: Claude Opus 5
(cherry picked from commit b05e0333c4b94d58eb794f09a2459e41d826c655)
2026-08-26 07:46:32 -04:00
|
|
|
"verdicts": tous_verdicts,
|
|
|
|
|
"verdicts_failed": [
|
|
|
|
|
{"step": quality.event_step(e), "name": e["name"]}
|
|
|
|
|
for e in verdicts_ratés
|
|
|
|
|
],
|
[ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.
« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.
Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.
Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.
--- EN ---
The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.
« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.
Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.
The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.
Assisted-by: Claude Opus 5
2026-08-24 07:21:37 -04:00
|
|
|
},
|
|
|
|
|
indent=2,
|
|
|
|
|
ensure_ascii=False,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
colour = sys.stdout.isatty() and not args.no_color
|
|
|
|
|
print(render(args.database, resultats, version, colour))
|
|
|
|
|
return 1 if trouve else 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(main())
|