`make_index_name` rend `{table}__{colonne}_index` depuis la 17 — deux
soulignés. Le nouvel index est créé, l'ancien reste. Mesuré sur une
chaîne 12 → 18 : 1 paire en 12, 3 en 16, 370 en 17, 365 en 18. Inerte à
la lecture, coûteux à l'écriture — deux arbres B entretenus à chaque
INSERT — et c'est le seul défaut de la famille qui empire tout seul.
L'outil s'abstient plus qu'il ne supprime : contrainte, clé primaire,
index partiel ou calculé, méthode ou classe d'opérateurs différentes,
convention non reconnaissable. Sur la base réelle, 367 sûrs sur 376.
Éprouvé sur une copie : 364 retirés, 10 Mo libérés, les 6301 contraintes
identiques au nom près, Odoo charge sans une ligne de journal, et un
« -u all » complet n'en recrée aucun.
--- EN ---
`make_index_name` returns `{table}__{column}_index` since 17 — two
underscores. The new index is created, the old one stays. Measured on a
12 → 18 chain: 1 pair in 12, 3 in 16, 370 in 17, 365 in 18. Inert on
read, costly on write — two B-trees maintained per INSERT — and the only
defect of this family that worsens on its own.
The tool abstains more than it drops: constraint, primary key, partial or
computed index, differing access method or operator class, unrecognisable
convention. On the real database, 367 safe out of 376.
Proven on a copy: 364 dropped, 10 MB freed, the 6301 constraints
identical name for name, Odoo loads without a single log line, and a full
« -u all » recreates none of them.
Assisted-by: Claude Opus 5
`check_cow_views` ne voyait que la rupture qui ARRÊTE la migration : la
forme que l'arch doit avoir change. Deux autres la laissent finir sans un
mot — un ancrage qu'une vue héritière de la cible réclame, un t-call vers
un gabarit que la cible ne livre plus — et ne se voient qu'à l'ouverture
de la page, c'est-à-dire jamais avant la fin.
Rejouée sur la chaîne 12 → 18, la prédiction nomme le défaut de /contact
dès le palier 13 → 14, en 0,8 s. Il avait attendu six paliers.
Ces copies ne sont PAS neutralisées : chacune porte une page écrite par
quelqu'un. Le pilote appelle la réparation après OpenUpgrade, là où la
vue module porte enfin l'arch de la cible.
--- EN ---
`check_cow_views` only saw the break that STOPS the migration: the shape
the arch must have changes. Two others let it finish without a word — an
anchor a target child view requires, a t-call to a template the target no
longer ships — and only show when the page is opened, which is to say
never before the end.
Replayed on the 12 → 18 chain, the prediction names /contact's defect as
early as the 13 → 14 step, in 0.8 s. It had waited six steps.
Those copies are NOT neutralized: each holds a page someone wrote. The
driver calls the repair after OpenUpgrade, where the module view finally
carries the target's arch.
Assisted-by: Claude Opus 5
`check_cow_views` cherche la rupture qui ARRÊTE la migration : la forme
que l'arch doit avoir change. Deux autres la laissent finir sans un mot.
Un ancrage manquant : une vue héritière fait un xpath sur `//t[@t-set=…]`
que le module a gagné en chemin ; la copie, page d'utilisateur, n'est
jamais réécrite. Un t-call pendant : le gabarit appelé n'existe plus dans
la cible. Mesuré sur une 12 → 18 réelle : /contact rendait 500 depuis le
palier 14 → 15, quatre paliers de silence, et le test de fumée le voyait
sans savoir le nommer.
Le premier se répare depuis la vue module, en gardant la page telle que
son auteur l'a écrite. Le second se retire, et l'humain décide.
--- EN ---
`check_cow_views` looks for the break that STOPS the migration: the shape
the arch must have changes. Two others let it finish without a word.
A missing anchor: a child view xpaths on `//t[@t-set=…]` that the module
gained along the way; the copy, a user page, is never rewritten. A
dangling t-call: the template called no longer exists in the target.
Measured on a real 12 → 18: /contact returned 500 since the 14 → 15 step,
four steps of silence, and the smoke test saw it without naming it.
The first is repaired from the module view, keeping the page as its
author wrote it. The second is removed, and the human decides.
Assisted-by: Claude Opus 5
Monter en 18 mourait sur « MuK Backend Theme et Web Responsive sont
incompatibles ». muk_web_theme n'excluait que web_enterprise en 16 et
en 17 ; la 18 y ajoute web_responsive. Les deux cohabitaient donc
légalement depuis la 12. On retire web_responsive au palier 17 → 18,
pendant que l'état est encore légal ; rien n'en dépend.
Deux défauts trouvés en le câblant. Odoo sort en 0 quand « --uninstall »
ne retire rien : muk_web_theme a traversé quatre paliers en étant réputé
parti. On lit maintenant l'état en base. Et les étapes désinstaller et
installer rangeaient leur drapeau sous la clé de l'étape migration —
à la reprise, OpenUpgrade était sauté pour ces paliers.
--- EN ---
Upgrading to 18 died on « MuK Backend Theme and Web Responsive are
incompatible ». muk_web_theme excluded only web_enterprise in 16 and 17;
18 adds web_responsive. The pair had been legal since 12. We drop
web_responsive at the 17 → 18 step, while the state is still legal;
nothing depends on it.
Wiring it up surfaced two defects. Odoo exits 0 when « --uninstall »
removes nothing: muk_web_theme crossed four steps while believed gone.
We now read the state back from the database. And the uninstall and
install steps stored their flag under the migrate step's key — on
resume, OpenUpgrade was skipped for those steps.
Assisted-by: Claude Opus 5
OpenUpgrade's website post-migration gathers the href of every page
anchor and glues them into a CSS selector:
"selector": ", ".join([link.attrib["href"] for link in links])
An href is not a selector. A French anchor written #principes-mn%C3%A9-
moniques puts a % in it, which no CSS identifier may hold, and the whole
migration dies on SelectorSyntaxError.
Proven by replaying that exact code on the database: three views failed
before, one of them with the % at position 53 -- the very position in
the log. After the fix, three views processed, none failed. Decoding is
enough: the parser accepts #principes-mnémoniques and refuses the
encoded form.
Only page views, only anchor hrefs, only %XX in hex. A % elsewhere in a
URL is legitimate and stays. The decoder lives in pg_temp and leaves
nothing behind.
--- FR ---
La post-migration website d'OpenUpgrade ramasse les href des ancres
d'une page et les recolle en sélecteur CSS. Or un href n'est pas un
sélecteur : une ancre française encodée y met un %, interdit dans un
identifiant CSS, et la migration meurt.
Prouvé en rejouant ce code exact sur la base : trois vues échouaient,
dont une avec le % en position 53 — celle du journal. Après correctif,
trois vues traitées, zéro échec. Décoder suffit : le parseur accepte
#principes-mnémoniques et refuse la forme encodée.
Seulement les vues de page, seulement les href d'ancre, seulement les
%XX hexadécimaux. Un % ailleurs dans une URL est légitime et reste. Le
décodeur vit dans pg_temp et ne laisse rien derrière lui.
Assisted-by: Claude Opus 5
My own regression. The tuple went from three fields to four and I
missed recheck_after_reset, so a live migration died on "too many values
to unpack" -- AFTER the COW copy had been reset, at the very moment the
tool was about to prove the fix worked.
Both sites wanted only the URL, so they index instead of unpacking. That
survives the next field; a comment saying "always four elements" does
not.
The pass had no test at all, which is why the suite and the mutations
both stayed green. It has three now, plus a guard that fails on any
three-field unpack of that list.
--- FR ---
Ma régression. Le tuple est passé de trois à quatre champs et j'ai
manqué recheck_after_reset : une migration en cours est morte sur « too
many values to unpack » — APRÈS la réinitialisation de la copie COW, au
moment précis où l'outil allait prouver que le correctif tenait.
Les deux sites ne voulaient que l'URL : ils indexent au lieu de
dépaqueter. Cela survit au prochain champ ; un commentaire « toujours
quatre éléments » non.
Cette passe n'avait aucun test, d'où le vert de la suite et des
mutations. Elle en a trois, plus un garde qui tombe sur tout
dépaquetage à trois champs de cette liste.
Assisted-by: Claude Opus 5
The report named the sitemap URL, never the one the redirect chain
ended on. On this site every page goes through two or three hops --
measured, 146 for 55 pages, between the language prefix and the
canonical slug -- so a 500 at the end was reported against a page that
answers perfectly well. One goes and checks it, finds it healthy, and
concludes the tool is wrong.
urllib carries the answer: HTTPError.url is the URL that produced the
error, not the one requested. fetch now returns it and the report shows
it when it differs.
A timeout was never reported as 500 -- fetch returns 0 and the report
writes "no answer" -- but the evidence for a real 500 did evaporate: the
Odoo log was opened with "w", so replaying the test erased the trace of
the failure one had just seen. One generation is kept now.
--- FR ---
Le rapport nommait l'URL du sitemap, jamais celle où la chaîne de
redirections aboutit. Sur ce site chaque page en traverse deux ou trois
— mesuré, 146 pour 55 pages — donc un 500 au bout était imputé à une
page qui répond très bien. On va la vérifier, on la trouve saine, et
l'on conclut que l'outil se trompe.
urllib porte la réponse : HTTPError.url est l'URL qui a produit
l'erreur. `fetch` la rend, et le rapport l'affiche quand elle diffère.
Un dépassement de délai n'a jamais été rendu comme un 500 — `fetch`
rend 0, écrit « aucune réponse » — mais la preuve d'un vrai 500
s'évaporait : le journal Odoo était ouvert en « w », donc rejouer le
test effaçait la trace qu'on venait de voir. Une génération est gardée.
Assisted-by: Claude Opus 5
OpenUpgrade converts the COLUMN and never the ARCH:
def _fix_list_view_type(cr):
cr.execute("UPDATE ir_ui_view SET type='list' WHERE type='tree'")
so the load dies on "the root node of a list view should be <list>".
A module view recovers at its next update, which rewrites the arch from
XML. A view with no xmlid -- a hand-made list, a website copy -- is
rewritten by nothing and stays broken forever. The report says which is
which, because the two call for different work.
Every occurrence, not just the root: a <tree> nested in a form is a
one2many list and is refused just the same. Odoo 18's own addons hold
none, so nothing legitimate is at risk. Gated on version 18: before it,
the tag is correct and "fixing" it would break healthy views.
Four tests run the SQL against a real PostgreSQL. A text assertion
cannot see what a query does -- it took a live run to prove every
language of the jsonb arch is converted, not just en_US.
--- FR ---
OpenUpgrade convertit la COLONNE et jamais l'ARCH, d'où l'échec sur « le
nœud racine d'une vue list devrait être <list> ». Une vue de module s'en
remet à sa prochaine mise à jour ; une vue sans xmlid — liste faite
main, copie de site — n'est réécrite par rien. Le rapport distingue les
deux, car le travail diffère.
Toutes les occurrences, pas seulement la racine : un <tree> imbriqué
dans un formulaire est une liste one2many, refusée pareillement. Les
addons d'Odoo 18 n'en portent aucun. Bridé à la 18 : avant, la balise
est juste.
Quatre tests exécutent le SQL contre un vrai PostgreSQL. Une assertion
sur du texte ne voit pas ce qu'une requête fait — il a fallu l'exécuter
pour prouver que toutes les langues du jsonb sont converties.
Assisted-by: Claude Opus 5
The question was asked once, at step one, and a flag kept it from ever
returning. But a theme only becomes incompatible at a given bump, hours
later -- asking at the start could not cover it. Entry [6] appears when
TWO conditions hold: a theme is installed, and the recent output names
it. One condition alone would offer to wreck the site's design over an
error that has nothing to do with it.
Also, the 17 to 18 rename OpenUpgrade declares and never applies:
forum.post / tag_ids : column1 is now 'forum_post_id' ('forum_id')
website_forum/18.0.1.2/ holds only an analysis, no script, so the load
dies on a foreign key to a column that does not exist and leaves the
database half migrated.
--- FR ---
La question était posée une fois, à l'étape 1, et un drapeau
l'empêchait de revenir. Or un thème ne devient incompatible qu'à un
palier donné, des heures plus tard : la poser au départ ne pouvait pas
suffire. L'entrée [6] paraît quand DEUX conditions tiennent — un thème
installé, et la sortie récente qui le nomme. Une seule offrirait de
casser le design du site pour une panne étrangère.
Et le renommage 17 → 18 qu'OpenUpgrade déclare sans jamais l'appliquer :
forum.post / tag_ids : column1 is now 'forum_post_id' ('forum_id')
website_forum/18.0.1.2/ ne porte qu'une analyse, aucun script : le
chargement meurt sur une clé étrangère vers une colonne absente et
laisse la base à moitié migrée.
Assisted-by: Claude Opus 5
Odoo refused to load a database: it validated a <search> arch with tree
rules. The view is not a COW copy -- the COW tools were right to say
they had nothing to reset -- its stored `type` simply lies.
Nothing would ever have fixed it. In ir_ui_view.py, `type` is filled in
`create`, and only when absent; `write` never recomputes it. A wrong
value stays wrong, and `-u module` fails on the validation it causes
before it could rewrite anything.
Plain SQL, no ORM: the registry is what will not load, and a repair that
needed Odoo to fix what stops Odoo would be useless.
The rule is Odoo's own -- an inherited view takes its parent's type --
and it holds: zero disagreement on a pristine 18 install and on four
migrated databases, exactly one on the database that refused to load.
--- FR ---
Odoo refusait de charger une base : il validait un arch <search> avec
les règles d'un tree. La vue n'est pas une copie COW — les outils COW
avaient raison de dire qu'ils n'avaient rien à réinitialiser — c'est son
`type` stocké qui ment.
Rien ne l'aurait jamais réparé. Dans ir_ui_view.py, `type` est rempli
dans `create`, et seulement s'il est absent ; `write` ne le recalcule
jamais. Une valeur fausse le reste, et `-u module` échoue sur la
validation qu'elle provoque avant de pouvoir réécrire quoi que ce soit.
En SQL, sans ORM : c'est le registre qui ne charge plus, et une
réparation qui aurait besoin d'Odoo ne servirait à rien.
La règle est celle d'Odoo — une vue héritée prend le type de son parent
— et elle tient : zéro écart sur une 18 neuve et sur quatre bases
migrées, exactement un sur celle qui refusait de charger.
Assisted-by: Claude Opus 5
The repair now runs by itself where the breakage happens: MuK becomes
OCA DMS at 13 and nowhere else, so the hook is gated on that bump. It
stays quiet on databases without DMS and on ones already repaired, and
a successful --apply now exits 0 instead of 1.
The wider hole stays open otherwise: data PRESENT that a global rule
hides entirely. Row counts said everything was there — true. The smoke
test served every page — true. Nobody could reach the records. The new
check runs at every bump and names any model with rows that no active
internal user can see a single one of. Proven both ways on a copy: 85
models clean after the repair, dms.file and dms.directory named before
it.
--- FR ---
La réparation s'exécute désormais là où la casse a lieu : MuK devient
OCA DMS au palier 13 et nulle part ailleurs, d'où le garde. Elle se tait
sur une base sans DMS et sur une base déjà réparée, et un --apply réussi
rend 0 au lieu de 1.
Le trou plus large restait ouvert : des données PRÉSENTES qu'une règle
globale masque entièrement. Les comptages disaient « tout est là » —
vrai. Le test de fumée servait les pages — vrai. Personne n'atteignait
les données. Le nouveau test tourne à chaque palier et nomme tout modèle
dont aucun utilisateur interne actif ne voit une seule ligne. Éprouvé
dans les deux sens sur une copie : 85 modèles sains après réparation,
dms.file et dms.directory nommés avant.
Assisted-by: Claude Opus 5
Nothing was lost at the 13 bump: 69 files, 16 folders and 23 MB of
content_binary are identical from 12 to 18. What changed is the security
model. MuK filtered on company alone; OCA DMS adds a GLOBAL rule on
permission_read, granted only through a dms.access.group or through
storages saved as attachment. The migration created no group — MuK had
none to convert — and the storages are database. Both doors shut, every
user sees zero.
Reports without writing unless --apply. The visibility count uses a real
user: the global rule spares the superuser, so a sudo count would call a
mute database healthy.
--- FR ---
Rien n'a été perdu au palier 13 : 69 fichiers, 16 dossiers et 23 Mo de
content_binary sont identiques de la 12 à la 18. C'est le modèle de
sécurité qui a changé. MuK ne filtrait que sur la société ; OCA DMS
ajoute une règle GLOBALE sur permission_read, accordée par une
dms.access.group ou par un stockage en attachment. La migration n'a créé
aucun groupe — MuK n'en avait aucun à convertir — et les stockages sont
en database. Les deux portes fermées, personne ne voit rien.
Rapport sans écriture sauf --apply. Le comptage de visibilité passe par
un vrai utilisateur : la règle globale épargne le super-utilisateur, donc
un comptage en sudo déclarerait saine une base muette.
Assisted-by: Claude Opus 5
No timeout was hit and no test was skipped. The back office failed for the
same reason the site did: the COW copy of website.submenu breaks
/web/login exactly as it breaks the public pages — both go through the
same layout. The pass simply ran BEFORE the reset, and unlike the URLs it
was never looked at again. It is now, on the same second server.
« Session expired » described the consequence and hid the cause. The login
page returned 500 and nothing said so, so one looks at the password. The
status of that page, and a missing CSRF token, are now named.
Diagnosing this, I started Odoo 18 on a 17 database and got 500 on all
thirty-seven URLs and on /web/login: a report that says the site is
entirely broken when nothing is. database_cleanup already refused that;
the smoke tools now share the same guard rather than reimplementing it.
--- FR ---
[FIX] smoke : juger le back-office APRÈS la réparation, et dire pourquoi
Aucun délai n'a été atteint et aucun test n'a été sauté. Le back-office a
échoué pour la raison même qui cassait le site : la copie COW de
website.submenu casse /web/login comme elle casse les pages publiques —
les deux passent par le même gabarit. La passe tournait avant la
réinitialisation et, contrairement aux URL, n'était jamais revue.
« Session expired » décrivait la conséquence et cachait la cause : la page
de connexion rendait 500 sans que rien ne le dise, et l'on cherchait du
côté du mot de passe.
En diagnostiquant, j'ai lancé un Odoo 18 sur une base 17 : 500 partout, un
rapport qui déclare le site entièrement cassé quand rien ne l'est.
database_cleanup refusait déjà cela ; les outils de fumée partagent
désormais sa garde au lieu d'en écrire une seconde.
Assisted-by: Claude Opus 5
The back-office pass was already there, and it works. What did not work
was the way it declined: one discreet line at the end of a long report,
saying « the database was not neutralized ». On six databases of a real
migration the test user is present up to the 15 bump and GONE at 17 and
18 — so the pass stopped silently exactly where a migration does the most
damage, and said something that was not even true.
The migration knows what it neutralized, so it now asks for the back
office by name. A missing test user on a database it neutralized is a
finding, printed loudly and counted as a failure. A missing tool is too:
returning None made the whole pass vanish without a word.
And it says UP FRONT which passes will run, on that database, by name.
--- FR ---
[FIX] migration : un back-office sauté ne doit pas se lire comme un sain
La passe back-office était déjà là et elle fonctionne. Ce qui ne
fonctionnait pas, c'est sa façon de renoncer : une ligne discrète en fin
d'un long rapport, disant « la base n'a pas été neutralisée ». Sur les six
bases d'une vraie migration, l'utilisateur test est présent jusqu'au
palier 15 et ABSENT en 17 et 18 — la passe s'arrêtait donc sans bruit là
où une migration fait le plus de dégâts, en disant quelque chose de faux.
La migration sait ce qu'elle a neutralisé : elle réclame désormais le
back-office. Un utilisateur test manquant sur une base qu'elle a
neutralisée est une trouvaille, affichée fort et comptée comme un échec.
Un outil absent aussi : rendre None faisait disparaître la passe entière.
Et elle annonce AVANT de lancer ce qui sera parcouru.
Assisted-by: Claude Opus 5
The failure that stops an upgrade is almost always the same one — a COW
copy left behind on the previous version — and the repair is known. So
Enter now repairs, and a repair that actually changed something replays
the command by itself.
A default that acts must know when to stop, and here it must twice over.
Repairing when there is nothing left to repair, then offering it again,
loops without end: measured, « no COW copy has drifted » over and over.
And a repair that does not help would replay the command forever. Three
attempts, then the prompt says plainly that this one needs a developer.
The turn counter is deliberately redundant with that logic: both guard
the same failure, but the counter holds even if the logic is broken one
day by accident. An endless loop in an unattended migration costs a
night.
Also: the smoke tool forced `ask=input` on its own prompt, which
short-circuited auto-run — the question waited for a keystroke nobody
was there to give.
--- FR ---
[FIX] migration : réparer, rejouer, et savoir s'arrêter
L'échec qui arrête une migration est presque toujours le même — une copie
COW restée sur la version d'avant — et la réparation est connue. Entrée
répare donc, et une réparation qui a vraiment changé quelque chose rejoue
la commande d'elle-même.
Un défaut qui agit doit savoir s'arrêter, et ici deux fois plutôt qu'une.
Réparer quand il n'y a plus rien à réparer, puis le reproposer, boucle
sans fin : mesuré, « Aucune copie COW n'a dérivé », encore et encore. Et
une réparation qui ne suffit pas rejouerait indéfiniment. Trois
tentatives, puis l'invite dit qu'il faut un développeur.
Le compteur de tours double volontairement cette logique : il tient même
si elle est cassée un jour par mégarde.
Assisted-by: Claude Opus 5
Seventeen minutes with no output looks exactly like an infinite loop. It
was not one: measured on the running process, zero lock waits and queries
changing every sample. It was working, silently, one module every thirty
seconds.
Both halves were mine. Purging line by line — added so one refusal could
not sink a category — calls button_immediate_uninstall() per line, and
that reloads the WHOLE registry: 5984 modules re-read each time. The
batch is now purged in one call, exactly as the OCA wizard intends, and
the per-line isolation only costs when a refusal actually happens.
And the tool captured its child's output, so nothing showed before the
end. It now relays each step as it arrives, with elapsed seconds, and a
timer kills the process group when the deadline passes.
--- FR ---
[FIX] database_cleanup : purger le lot, et dire ce qu'on fait
Dix-sept minutes sans une ligne ressemblent trait pour trait à une boucle
infinie. Ce n'en était pas une : mesuré sur le processus vivant, zéro
verrou en attente et des requêtes qui changeaient à chaque instantané. Il
travaillait, en silence, à raison d'un module toutes les trente secondes.
Les deux moitiés étaient de mon fait. Purger ligne par ligne — ajouté
pour qu'un refus n'emporte pas la catégorie — appelle
button_immediate_uninstall() par ligne, ce qui recharge TOUT le registre :
5984 modules relus à chaque fois. Le lot est désormais purgé en un seul
appel, comme le module OCA le prévoit, et l'isolement ne coûte que
lorsqu'un refus survient vraiment.
Et l'outil capturait la sortie de son enfant : rien avant la fin. Il
relaie maintenant chaque étape, temps écoulé compris.
Assisted-by: Claude Opus 5
The portal is a third rendering, neither the public site nor the back
office: QWeb frontend with counters that each query their own model. The
sitemap does not list /my — the page needs a session — and no RPC call
goes through it. A migration can break it with nothing else noticing.
Measured on a real database: /my answered 500 on a user field a module no
longer defines, while every public page and sixteen apps were fine.
Two guards the run proved necessary. Requested without a session, /my
redirects to the login form and answers 200: counting that as a success
would announce a healthy portal never seen. And the production error page
shows no traceback, so the reason comes from the server log — attributed
by PATH, because the portal is checked first and taking the last trace
blamed it for an app's failure. A wrong cause costs more than none.
--- FR ---
[ADD] migration : ouvrir /my avec l'utilisateur test, et accuser la bonne requête
Le portail est un troisième rendu, ni le site public ni le back-office :
du QWeb frontend, avec ses compteurs qui interrogent chacun leur modèle.
Le sitemap ne liste pas /my — la page demande une session — et aucun appel
RPC n'y passe. Une migration peut le casser sans que rien ne le dise.
Mesuré sur une vraie base : /my rendait 500 sur un champ utilisateur qu'un
module ne définit plus, alors que les pages publiques et seize
applications allaient bien.
Deux garde-fous que l'essai a rendus nécessaires. Sans session, /my
redirige vers le formulaire de connexion et rend 200 : le compter pour une
réussite annoncerait un portail jamais vu. Et la page d'erreur de
production ne montre aucune trace : la raison vient du journal, attribuée
par CHEMIN — le portail est interrogé en premier, et prendre la dernière
trace l'accusait de la panne d'une application.
Assisted-by: Claude Opus 5
The public smoke test opens what a visitor reaches. It says nothing about
the back office, which is where a migration does most of its damage: a
field dropped from a model but still named in a form, a module installed
in the database whose code no longer ships with the target version. None
of it stops the module loading — it stops the day someone opens the app.
Neutralizing installs a `test` / `test` login carrying the SYSTEM user's
groups, and it survives the module's uninstall. So the tool checks for
that user rather than trusting a flag, and it rides the server the public
pass already started: booting Odoo is what costs minutes, not requests.
Measured on a real 18.0 database of 25 apps, which found four defects of
mine: web_search_read changed signature in 17, a bare 404 hides an
unregistered model, embedded sub-views are not the parent's fields, and
an empty psql result meant two different things.
--- FR ---
[ADD] migration : ouvrir chaque application avec l'utilisateur test
Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du
back-office, où une migration fait pourtant l'essentiel de ses dégâts : un
champ retiré du modèle mais toujours nommé dans un formulaire, un module
installé en base dont le code n'accompagne plus la version cible. Rien de
cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli.
La neutralisation pose un compte `test` / `test` portant les groupes du
superutilisateur, et il survit à la désinstallation du module. L'outil
vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur
déjà démarré : c'est le démarrage qui coûte, pas les requêtes.
Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre
défauts de mon fait.
Assisted-by: Claude Opus 5
Auto-run promised to "take the default after five seconds", but every
default was EMPTY: it took nothing. Worse, half the prompts of a
migration are asked by separate processes — the theme uninstaller, the
stale-SCSS detector, the smoke test. They knew nothing of auto-run and
waited forever for a keystroke that never came.
The countdown now lives in one file and travels through the ENVIRONMENT,
the only channel a fork shares. Enter takes the default everywhere, not
only under auto-run: a prompt that prints (Y/n) and does otherwise is
worse than no prompt. Every text was rewritten to say what Enter does,
and every default kept an explicit way out.
Two defaults now write. Both are tenable only because the backup runs
first, and a test locks that ORDER rather than trusting a promise.
--- FR ---
[ADD] migration : faire d'Entrée la réponse qu'on donne toujours
L'auto-exécution promettait « le défaut après cinq secondes », mais tous
les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des
invites d'une migration sont posées par d'autres processus — le
désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils
ignoraient l'auto-exécution et attendaient sans fin une frappe.
Le compte à rebours tient désormais dans un seul fichier et voyage par
l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut
partout, pas seulement en auto : une invite qui affiche (Y/n) et fait
l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée,
et chaque défaut garde une issue explicite.
Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe
d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse.
Assisted-by: Claude Opus 5
One refusal killed seven categories: modules died on « savepoint ...
does not exist », then columns, tables, data, menus, indexes and
properties all reported « current transaction is aborted ». Six
victims that never got to try.
The savepoints were mine and they could not work. The OCA module
commits on its own — purge_columns.py:57 calls cr.commit(), and
purge_modules.find() purges at line 91 before returning. A COMMIT
destroys every savepoint, so ours vanished under our feet and nothing
put the transaction back on its feet.
Each entry is committed as it succeeds, and any failure rolls back.
The dry run rolls back too: find() was writing for real.
--- FR ---
[FIX] database_cleanup : rattraper la transaction, pas le point de reprise
Un seul refus en tuait sept : modules mourait sur « savepoint ... does
not exist », puis colonnes, tables, données, menus, index et
propriétés signalaient tous « current transaction is aborted ». Six
victimes qui n'ont jamais eu leur tour.
Les points de reprise étaient les miens et ne pouvaient pas tenir. Le
module OCA valide de lui-même — purge_columns.py:57 appelle cr.commit(),
et purge_modules.find() purge dès la ligne 91. Or un COMMIT détruit
tout point de reprise : le nôtre disparaissait sous nos pieds, et rien
ne remettait la transaction d'aplomb.
Chaque entrée est validée dès qu'elle réussit, tout échec défait la
sienne. La simulation défait aussi : find() écrivait pour de bon.
Assisted-by: Claude Opus 5
Three defects, all mine, all found on a real run.
Creating a wizard could fail and leave the transaction ABORTED. The next
name read died on it, outside any guard, and the whole script stopped
with no report at all — only a traceback. Creating and reading the names
now happen inside the savepoint, and a report is printed whatever
happens: knowing what was done matters more than the trace of what
broke.
« No orphaned models found » is a UserError: the module signals the
EMPTY by raising. Counting it as a failure made a healthy database look
broken, four warnings out of five kinds.
The module was installed at step 3, after being used at step 2, so the
first run had no wizard at all and answered « nothing to do ». It is now
installed by the tool itself, and step 3 no longer asks to redo by hand
what has just been done automatically.
--- FR ---
[FIX] migration : le nettoyage pose son module, et survit à un refus
Trois défauts, tous à moi, tous trouvés sur une vraie exécution.
Créer un assistant pouvait échouer en laissant la transaction AVORTÉE.
La lecture de nom suivante mourait dessus, hors de tout garde, et le
script s'arrêtait sans aucun rapport — juste une trace. La création et
la lecture des noms sont désormais dans le point de reprise, et un
rapport est imprimé quoi qu'il arrive : savoir ce qui a été fait vaut
mieux que la trace de ce qui a cassé.
« No orphaned models found » est une UserError : le module signale le
VIDE en levant. Le compter comme un échec faisait passer une base saine
pour cassée, quatre avertissements sur cinq catégories.
Le module était installé à l'étape 3, après avoir servi à l'étape 2 : le
premier passage n'avait donc aucun assistant et répondait « rien à
faire ». L'outil le pose lui-même, et l'étape 3 ne demande plus de
refaire à la main ce qui vient d'être fait.
Assisted-by: Claude Opus 5
The eight purges are not independent: purging a model frees the columns
that referenced it, purging a table frees the data pointing at it. One
pass is never enough, so the requested order runs again until a full
pass repairs nothing new.
Refusals are expected — a foreign key still holds, a module says no.
Purging a list at once loses everything to the first one, so each entry
purges inside its own savepoint: a refusal rolls back that entry alone.
What one pass could not take, the next may, once its neighbours are
gone. Leftovers are reported as a warning: a database can carry some
that nothing removes, and stopping there would help no one.
It refuses a version mismatch. Measured while building it: a shell on
Odoo 14 opened against a 17.0 database went rewriting ir_model before
dying on a jsonb it did not know. An older Odoo does not merely fail on
a newer database — it writes on the way.
--- FR ---
[ADD] migration : lancer le nettoyage OCA avant de tester les pages
Les huit purges ne sont pas indépendantes : purger un modèle libère les
colonnes qui le référençaient, purger une table libère les données qui
la visaient. Une passe ne suffit jamais, l'ordre demandé est donc rejoué
jusqu'à ce qu'une passe entière ne répare plus rien.
Les refus sont attendus — une clé étrangère tient, un module dit non.
Purger d'un bloc perdrait tout au premier : chaque entrée passe dans son
propre point de reprise, un refus n'emporte que la sienne. Ce qu'une
passe n'a pu prendre, la suivante le peut, une fois les voisines
parties. Les restes sont un avertissement : une base peut en porter que
rien ne retire, et s'arrêter là n'aiderait personne.
Il refuse une version qui ne correspond pas. Mesuré en le construisant :
un shell en Odoo 14 ouvert sur une base 17.0 est parti réécrire ir_model
avant de mourir sur un jsonb inconnu de lui. Un Odoo plus ancien
n'échoue pas simplement sur une base plus récente — il écrit en chemin.
Assisted-by: Claude Opus 5
A QWebException carries no [view_id … parent_id …] block — it names the
template. The tool only read the inheritance context, so it answered
« no parent view named » on the one failure that hit everything.
Measured at bump 17: a frozen copy of website.submenu still called
submenu.clean_url(), renamed _clean_url() in that version. Every page
renders the menu, so 34 of 37 public URLs returned 500, and nothing
pointed at the view. Resetting that one copy brought all 37 back.
Only keys that actually have a COW copy are proposed: naming one
without would send a reset against a module view, which does nothing.
--- FR ---
[FIX] migration : nommer le gabarit quand l'échec vient du rendu
Une QWebException ne porte pas de bloc [view_id … parent_id …] : elle
nomme le gabarit. L'outil ne lisait que le contexte d'héritage, et
répondait donc « aucune vue parente nommée » sur la seule panne qui
touchait tout.
Mesuré au palier 17 : une copie figée de website.submenu appelait
encore submenu.clean_url(), renommée _clean_url() par la version. Toute
page affiche le menu, donc 34 URL publiques sur 37 rendaient 500, sans
que rien ne désigne la vue. Réinitialiser cette seule copie les a
toutes ramenées.
Seules les clés ayant vraiment une copie COW sont proposées : en nommer
une sans copie enverrait réinitialiser une vue module, donc ne rien
faire.
Assisted-by: Claude Opus 5
The text report puts a thousand lines between the two things that decide:
what the copy holds that the module view does not — all a reset gives up
— and which child no longer finds its anchor, which is why anything
breaks. Space switches between them, « c » copies the reset command.
Offered as [4] on the error prompt, next to check and reset, and run on
the real terminal: a full screen started through the capturing executor
falls back to the text report with nothing to tell the two apart.
The diff is rendered in one place, shared with the command line. Two
renderings would drift without anything saying so.
--- FR ---
[ADD] migration : parcourir les copies COW dérivées en plein écran
Le rapport texte met mille lignes entre les deux choses qui décident :
ce que la copie porte et que la vue module n'a pas — tout ce qu'une
réinitialisation abandonne — et quel enfant ne trouve plus son ancrage,
la raison pour laquelle quoi que ce soit casse. L'espace bascule, « c »
copie la commande de réparation.
Proposé en [4] à l'invite d'erreur, à côté de vérifier et réinitialiser,
et lancé sur le vrai terminal : un plein écran passé par l'exécuteur qui
capture retombe sur le rapport texte, sans rien qui les distingue.
Le diff est rendu à un seul endroit, partagé avec la ligne de commande.
Deux rendus dériveraient sans que rien ne le signale.
Assisted-by: Claude Opus 5
Answering « all » reset one copy of two and reported success. Two
defects behind it, both mine.
A requested key matching no finding did nothing, silently: the tool
returned early on « nothing drifted » and never looked. Detection is
differential — it only sees a copy whose CHILD breaks — so a stale copy
without children escapes it. A key can now be reset outside detection,
an unknown one is named, and the exit code is 2.
And the culprit is not always the parent. On /contactus the parent was
identical to its module view; the CHILD held the stale arch. Both are
proposed now, parent first — it is the more common case.
Measured on the migration: 33 of 33 public URLs answer.
--- FR ---
[FIX] migration : réinitialiser la copie vraiment périmée, et signaler une clé sans objet
Répondre « toutes » réinitialisait une copie sur deux et annonçait un
succès. Deux défauts derrière, tous deux à moi.
Une clé demandée ne correspondant à aucun constat ne faisait rien, en
silence : l'outil sortait sur « rien n'a dérivé » sans jamais chercher.
La détection est différentielle — elle ne voit qu'une copie dont un
ENFANT casse — donc une copie périmée sans enfant lui échappe. Une clé
peut désormais être réinitialisée hors détection, une clé inconnue est
nommée, et le code de sortie vaut 2.
Et le coupable n'est pas toujours le parent. Sur /contactus, le parent
était identique à sa vue module ; c'est l'ENFANT qui portait l'arch
périmée. Les deux sont proposés, le parent d'abord — le cas le plus
fréquent.
Mesuré sur la migration : 33 URL publiques sur 33 répondent.
Assisted-by: Claude Opus 5
The theme uninstaller ends by asking what to do with the leftovers, and
the migration ran it through the capturing executor. Its stdout was a
pipe, Python buffers by blocks, so the prompt stayed invisible while the
process waited. It reads as a freeze: you press Enter blind, the first
keystroke answers unseen and the extra ones fall into the next question.
Two locks, because one is not enough. The migration runs the script on
the real terminal. And the three tools that ask now require BOTH ends:
something to read the answer from, and something to show the question
on. Guarding on stdin alone was the bug — measured, stdin tty=True with
stdout tty=False, and the question was asked into a pipe.
--- FR ---
[FIX] migration : ne jamais poser une question que le terminal ne montre pas
Le désinstalleur de thème finit par demander quoi faire des restes, et
la migration le lançait par l'exécuteur qui capture. Sa sortie partait
dans un tube, Python bufferise par blocs, et l'invite restait invisible
pendant l'attente. Cela se lit comme un blocage : on tape Entrée à
l'aveugle, la première frappe répond sans être vue et les suivantes
tombent dans la question d'après.
Deux verrous, car un seul ne suffit pas. La migration lance le script
sur le vrai terminal. Et les trois outils qui questionnent exigent
désormais les DEUX bouts : de quoi lire la réponse, et de quoi montrer
la question. Ne garder que stdin était le défaut — mesuré, stdin
tty=True et stdout tty=False, la question partait dans un tube.
Assisted-by: Claude Opus 5
The smoke test stopped at « these URLs answer 500 ». Turning that into a
fix meant reading an id out of a traceback and translating it to a key
by hand, mid-migration — the copy where a character goes missing.
It now reads the error context Odoo logs — [view_id: N, parent_id: M],
the one line it does not translate — resolves the parent to its key,
offers the reset numbered with « all », and re-requests the failing URLs
afterwards. Applying without re-asking would be calling it fixed
without having seen it answer.
Two defects found while measuring, both mine. ./run.sh is a bash
wrapper: terminate() killed it and left odoo-bin holding the port, six
orphans in six runs, each later run silently querying the first one's
server. And the log was read before stopping, so only the 24 startup
lines existed — hence « no view in cause » on pages that named one.
--- FR ---
[ADD] migration : nommer les vues derrière une URL en échec, et corriger
Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif
demandait de relever un id dans une trace et de le traduire en clé à la
main, en pleine migration — la recopie où un caractère se perd.
Il lit désormais le contexte qu'Odoo journalise — [view_id: N,
parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent
en clé, propose la réinitialisation numérotée avec « toutes », et
redemande ensuite les URL en échec. Appliquer sans redemander, ce
serait déclarer réparé sans l'avoir vu répondre.
Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une
enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le
port, six orphelins en six essais, chaque essai suivant interrogeant
sans le savoir le serveur du premier. Et le journal était lu avant
l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où
« aucune vue en cause » sur des pages qui en nommaient une.
Assisted-by: Claude Opus 5
(cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
A migration can load every module, log nothing, and still serve 500s on
pages nobody thought to open. Measured on the real one: 2 of 33 public
URLs failed — a blog post and /contactus — after a bump the log called
successful.
The list is the sitemap, what Odoo publishes for search engines. It
cannot be read from odoo-bin shell: enumerate_pages() asks for
http.root.get_db_router(request.db) and raises « object unbound »
without a real request. So the server is started on its own port,
asked, and always stopped — a forgotten one holds the port and fails
the next bump.
Offered before the Selenium prompt, default no: it boots a server and
can take minutes.
--- FR ---
[ADD] migration : interroger chaque URL publique avant de conclure
Une migration peut charger tous ses modules, ne rien écrire au journal,
et servir quand même des 500 sur des pages que personne n'ouvre.
Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et
/contactus — après un palier que le journal disait réussi.
La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne
se lit pas depuis odoo-bin shell : enumerate_pages() réclame
http.root.get_db_router(request.db) et lève « object unbound » sans
requête réelle. Le serveur est donc démarré sur son propre port,
interrogé, et toujours arrêté — un serveur oublié tient le port et fait
échouer le palier suivant.
Proposé avant l'invite Selenium, par défaut non : cela démarre un
serveur et peut durer.
Assisted-by: Claude Opus 5
(cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
The error prompt printed « --reset <key> --apply » and left the key to
be found in a thousand-line diff. Copying it by hand is where a
character goes missing, and a key matching no copy is not an error for
the tool: the command runs and does nothing, silently.
[3] now asks the tool for the keys, numbers them, and offers « all ».
An out-of-range or unreadable answer resets nothing — falling back to
« all » would act on what was never asked for.
--- FR ---
[ADD] migration : choisir la copie COW à réinitialiser dans une liste
L'invite d'erreur imprimait « --reset <key> --apply » et laissait
retrouver la clé dans un diff de mille lignes. La recopier à la main
est l'endroit où un caractère se perd, et une clé ne correspondant à
aucune copie n'est pas une erreur pour l'outil : la commande tourne et
ne fait rien, en silence.
[3] demande désormais les clés à l'outil, les numérote et offre
« toutes ». Une réponse hors liste ou illisible ne réinitialise rien —
retomber sur « toutes » agirait sur ce qui n'a pas été demandé.
Assisted-by: Claude Opus 5
(cherry picked from commit ae08e13fe5bba2b02b8661c67c4024fcd79f29bc)
The fix was offered while the checkout was still on the previous
version. Answering « a » ran reset_asset in an Odoo 12 shell, which has
no web_editor.assets: KeyError, nothing changed, and the migration went
on to break at the next bump — measured on a real run.
Predicting early is right; fixing early is not. The early call is now
--report-only, and a second call comes after the bump, on the upgraded
database, where the checkout can do it. The tool also refuses on its
own: it reads the checkout sources for reset_asset rather than trusting
a version number, and says when to come back.
--- FR ---
[FIX] migration : réinitialiser le SCSS périmé après le palier, pas avant
La correction était proposée alors que le checkout était encore sur la
version précédente. Répondre « a » lançait reset_asset dans un shell
Odoo 12, sans web_editor.assets : KeyError, rien de modifié, et la
migration continuait jusqu'à casser au palier suivant. Mesuré.
Prédire tôt est juste ; corriger tôt ne l'est pas. L'appel précoce est
désormais --report-only, et un second vient après le palier, sur la
base montée de version, là où le checkout sait le faire. L'outil refuse
aussi de lui-même : il cherche reset_asset dans les sources du checkout
plutôt que de se fier à un numéro, et dit quand revenir.
Assisted-by: Claude Opus 5
(cherry picked from commit 13d4d0a33c63efcbafb5fb29313a5377ba5f508e)
The report named the attachment and printed a command to paste. Deciding
« reset it » still meant accepting to lose one did not know what: the
diff against the module file is the only thing resetting gives up.
The tool now shows it and asks. --diff prints it, --tui browses it,
--apply resets without asking; with no flag and a terminal it asks, and
looking does not answer — the prompt comes back after each read.
--apply writes the copies under private/ first: reset_asset deletes the
attachment, so without that the customized lines would be nowhere.
The migration runs it on the real terminal, or none of this would be
reachable from there — the same pipe that was closing the TUI.
--- FR ---
[ADD] migration : lire le SCSS périmé, puis le corriger, sans sortir
Le rapport nommait la pièce jointe et imprimait une commande à coller.
Répondre « réinitialise » revenait encore à accepter de perdre on ne
sait quoi : l'écart avec le fichier du module est la seule chose que la
réinitialisation abandonne.
L'outil le montre et pose la question. --diff l'imprime, --tui le
parcourt, --apply réinitialise sans demander ; sans drapeau et devant
un terminal il demande, et regarder ne répond pas — l'invite revient
après chaque lecture. --apply écrit d'abord les copies sous private/ :
reset_asset supprime la pièce jointe, sans quoi les lignes
personnalisées ne seraient plus nulle part.
La migration le lance sur le vrai terminal, sinon rien de tout cela n'y
serait atteignable — le tube même qui fermait la TUI.
Assisted-by: Claude Opus 5
(cherry picked from commit 9e6c88622a74f7be408a8b1c523838eeadda1da3)
Customized SCSS lives in ir_attachment, frozen the day it is written,
still using the variables of that version. A bump can rename them:
website declared $o-theme-font-number in 12.0 and replaced the whole
mechanism in 13.0, so a 2020 customization stopped the frontend
bundle. Same shape as a stale COW view.
Reading the stored SCSS against the target sources answers before the
bump and names the attachment. Booting Odoo and opening a page answers
after, with « Style error » and no name — on a page a broken frontend
bundle is exactly what keeps you from reaching.
Measured: on the pre-bump database, targeting 13.0, it predicts the
failure; against its own 12.0 sources it reports nothing. That noise
floor is what makes it usable — the first version cried wolf on six
mixin parameters and named @include arguments.
--- FR ---
[ADD] migration : prédire quel SCSS personnalisé le palier va casser
Un SCSS personnalisé vit dans ir_attachment, figé le jour où il est
écrit, employant encore les variables de cette version-là. Un palier
peut les renommer : website déclarait $o-theme-font-number en 12.0 et a
remplacé le mécanisme en 13.0, arrêtant le bundle sur une
personnalisation de 2020. Même forme qu'une vue COW périmée.
Lire le SCSS stocké contre les sources cibles répond AVANT le palier et
nomme la pièce jointe. Démarrer Odoo et ouvrir une page répond après,
par « Style error » sans nom — et un bundle cassé est justement ce qui
empêche d'atteindre la page.
Mesuré : sur la base d'avant le palier, cible 13.0, il prédit la
panne ; contre ses propres sources 12.0 il ne signale rien. Ce bruit de
fond nul est ce qui le rend utilisable — la première version criait à
tort sur six paramètres de mixin et arguments nommés.
Assisted-by: Claude Opus 5
(cherry picked from commit c6611ee1431cfee94e321536d20052b8cca3ec10)
Nine of the thirteen carried a shebang without the bit, so only four
could be run by their own path. Set to 755, not chmod +x: two were
664 under the 0002 umask and would have become 775 — executed by
others while a group member could still rewrite them. That pairing is
the only real risk here; the bit alone grants nothing, since reading
the file is enough to run python3 on it.
Being runnable opens a path without the venv: the shebang resolves to
the system python3, which has no Textual. --tui fell back to the text
report saying nothing. It now names the interpreter and the venv.
--- FR ---
[FIX] script : rendre exécutables les outils d'analyse et de migration
Neuf des treize portaient un shebang sans le bit ; quatre seulement
se lançaient par leur chemin. Mis à 755, pas chmod +x : deux étaient
en 664 sous l'umask 0002 et seraient passés à 775 — exécutés par
d'autres alors qu'un membre du groupe pouvait encore les réécrire.
C'est la seule vraie prise ici ; le bit seul n'accorde rien, lire le
fichier suffit déjà à lancer python3 dessus.
Devenir lançable ouvre un chemin sans le venv : le shebang résout le
python du système, sans Textual. --tui retombait sur le rapport texte
sans rien dire. Il nomme désormais l'interpréteur et le venv.
Assisted-by: Claude Opus 5
They run as subprocesses and had no i18n, so a French migration alternated
languages from one line to the next. The database upgrade itself had the
same gap.
The driver read English sentences out of their output to decide whether to
ask its question. Translating them would have made it mute -- no error, no
trace. The link is now the exit code, which no language touches: 0
nothing, 1 copies concerned, 2 the tool failed.
87 strings, five tools. A test rejects any displayed sentence that skips
t(), and proves itself on an untranslated one.
--- FR ---
Ils tournent en sous-processus et n'avaient aucun i18n : une migration en
français alternait les deux langues d'une ligne à l'autre. La mise à
niveau de la base elle-même souffrait du même manque.
Le pilote lisait des phrases anglaises dans leur sortie pour décider de
poser sa question. Les traduire l'aurait rendu muet — sans erreur, sans
trace. Le lien est désormais le code de sortie, qu'aucune langue ne
touche : 0 rien, 1 copies concernées, 2 l'outil a échoué.
87 chaînes, cinq outils. Un test refuse toute phrase affichée qui saute
t(), et fait sa preuve sur une chaîne non traduite.
Assisted-by: Claude Opus 5
Realising at a prompt that an earlier step deserved another answer had one
way out: Ctrl+C. That leaves the progression as it stands and makes you find
the resume screen again to rewind. « b » does it properly — it shows the
steps, rewinds the state, writes it, and says what to relaunch.
Cancelling that must not stop the migration, which is the trap: it returns to
the same prompt, exactly where you were. Removing that guard makes two tests
fail.
The COW warning printed « -d DB -t odooXX.0 » for commands meant to be pasted.
It knows both values, so it prints them, and offers --shape as well since that
is half the answer.
Checked on the four outcomes: a normal answer passes through, « b » then a
step rewinds and stops, cancelling and an unknown step both continue. 15 tests.
--- FR ---
S'apercevoir à une invite qu'une étape antérieure méritait un autre choix
n'avait qu'une issue : Ctrl+C. Cela laisse la progression telle quelle et
oblige à retrouver l'écran de reprise pour rembobiner. « b » le fait
proprement — il montre les étapes, rembobine l'état, l'écrit, et dit quoi
relancer.
Y renoncer ne doit pas arrêter la migration, et c'est le piège : on revient à
la même invite, exactement là où l'on était. Retirer ce garde-fou fait tomber
deux tests.
L'avertissement COW affichait « -d DB -t odooXX.0 » pour des commandes faites
pour être collées. Il connaît les deux valeurs, donc il les écrit, et propose
aussi --shape puisque c'est la moitié de la réponse.
Vérifié sur les quatre issues : une réponse normale passe, « b » puis une
étape rembobine et arrête, annuler et un choix inconnu continuent. 15 tests.
Assisted-by: Claude Opus 5
An early check warns, hours before the bump, that a copy will break it. It
then says « arbitrate BEFORE launching the migration » and hands over a raw
UPDATE — and no question follows. Read on a resumed run it looks like the
question already went by and was skipped, which is exactly how it was read.
It now says the migration asks at the bump itself, and shows what each copy
holds before the answer. The manual UPDATE stays, but after the two commands
that do it reversibly: telling someone to hand-write SQL when --restore exists
is offering the sharper tool first.
--- FR ---
Un contrôle précoce prévient, des heures avant le palier, qu'une copie va le
casser. Il dit ensuite « arbitrez AVANT de lancer la migration » et livre un
UPDATE brut — et aucune question ne suit. Lu au cours d'une reprise, on croit
que la question est passée et a été sautée, ce qui est exactement la lecture
qui en a été faite.
Il dit maintenant que la migration pose la question au palier lui-même, en
montrant ce que chaque copie contient avant qu'on réponde. L'UPDATE manuel
reste, mais après les deux commandes qui le font de façon réversible : dire à
quelqu'un d'écrire du SQL à la main quand --restore existe, c'est tendre
l'outil le plus coupant en premier.
Assisted-by: Claude Opus 5
The declarations view showed a 12.0 template becoming a 13.0 extension and
stopped there. Read alone it says « Odoo changed its code », and the obvious
reaction is to ask why that is anyone's problem — which is exactly what it
prompted.
It is not the problem. On a database without a copy the module upgrade
rewrites the view and nothing breaks. It breaks because a COPY exists and
froze the old shape. That sentence was missing, and so was the other half of
the answer: what the copy actually holds. A copy identical to the view it
shadows costs nothing to neutralize; one carrying five lines of theme hooks
costs those five lines. Both are now stated where the question arises.
Checked on the database mid-migration: the copy that raised the question
reports +5/-3, and the two other cases — identical to its twin, and a page
with no twin at all — each say so.
--- FR ---
La vue des déclarations montrait un gabarit 12.0 devenant une extension 13.0
et s'arrêtait là. Lue seule, elle dit « Odoo a changé son code », et la
réaction naturelle est de demander en quoi cela nous regarde — ce qu'elle a
justement provoqué.
Ce n'est pas le problème. Sur une base sans copie, la mise à jour du module
réécrit la vue et rien ne casse. Ça casse parce qu'une COPIE existe et a figé
l'ancienne forme. Cette phrase manquait, et l'autre moitié de la réponse
aussi : ce que la copie contient réellement. Une copie identique à la vue
qu'elle masque ne coûte rien à neutraliser ; une qui porte cinq lignes
d'ancres de thème coûte ces cinq lignes. Les deux sont dites là où la question
se pose.
Vérifié sur la base en cours de migration : la copie qui a soulevé la question
rapporte +5/-3, et les deux autres cas — identique à sa jumelle, et page sans
jumelle — le disent chacun.
Assisted-by: Claude Opus 5
The prompt asked whether to neutralize copies without showing what they hold.
Answering meant giving up a customization sight unseen — often three lines, an
id and a container width, sometimes a whole page, and nothing told them apart.
« v » now shows both halves of the question. What the copy changed, diffed
against the module view it shadows: on the database at hand, 5 lines added and
3 removed, two CSS anchors and a container width. And why it breaks, by
showing the declaration in each version — portal.frontend_layout is a
standalone template in 12.0 and inheritance specs in 13.0, which is the whole
explanation. « w » is the same two, full screen, space to switch.
The current version comes from ir_module_module, not .odoo-version: the
checkout is switched to the target before this runs, so reading the file
compared the target with itself and printed the same declaration twice. That
is what it did until it was run against a real migration.
--list says which copies a past neutralization put aside. A renamed key is
invisible in the interface, so without it the only trace was remembering.
Checked on the VM mid-migration: both views on view 2670, the screen builds
headless and toggles, --list reports and reports nothing when there is
nothing. A missing database now exits 2 with a message instead of a traceback.
--- FR ---
L'invite demandait de neutraliser des copies sans montrer ce qu'elles
contiennent. Répondre revenait à renoncer à une personnalisation sans l'avoir
vue — souvent trois lignes, un id et une largeur de conteneur, parfois une
page entière, et rien ne les distinguait.
« v » montre désormais les deux moitiés de la question. Ce que la copie a
changé, comparé à la vue de module qu'elle masque : sur la base en cours, 5
lignes ajoutées et 3 retirées, deux ancres CSS et une largeur. Et pourquoi ça
casse, en affichant la déclaration dans chaque version —
portal.frontend_layout est un gabarit autonome en 12.0 et des consignes
d'héritage en 13.0, ce qui est toute l'explication. « w » donne les deux en
plein écran, espace pour basculer.
La version courante vient d'ir_module_module, pas de .odoo-version : le
checkout est basculé sur la cible avant cette étape, donc lire le fichier
comparait la cible avec elle-même et affichait deux fois la même déclaration.
C'est ce qu'il faisait jusqu'à l'essai sur une vraie migration.
--list dit quelles copies une neutralisation passée a mises de côté. Une clé
renommée est invisible dans l'interface ; sans cela, la seule trace était de
s'en souvenir.
Vérifié sur la VM en cours de migration : les deux vues sur la vue 2670,
l'écran se construit sans terminal et bascule, --list rapporte et ne rapporte
rien quand il n'y a rien. Une base absente sort en 2 avec un message plutôt
qu'une trace d'appel.
Assisted-by: Claude Opus 5
A copy-on-write view freezes the module view it came from; the module moves
on and the upgrade dies hours later on a missing anchor. These tools
predict, snapshot, diff, neutralize and reset them. The migration screen
gains the real state of each step, replay from any of them, and statistics.
--- FR ---
Une vue copy-on-write fige la vue de module dont elle vient ; le module
évolue et la mise à niveau meurt des heures plus tard sur un point
d'ancrage absent. Ces outils les prévoient, photographient, comparent,
neutralisent et réinitialisent. L'écran de migration gagne l'état réel de
chaque étape, la reprise depuis n'importe laquelle, et des statistiques.
Assisted-by: Claude Opus 5
Reflect the current year in all TechnoLibre
license headers across script/, test/, and docker/.
Generated by Claude Code 2.1.74 model claude-sonnet-4-6
Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
Added 30 .base.md files using the mmg (Multilingual Markdown Generator)
format to automatically generate English (.md) and French (.fr.md)
versions of all project documentation.
Updated conf/make.documentation.Makefile to process all .base.md files
via `make doc_markdown`.
- add example odoo test
- prevent delete production file with validation
- add makefile with selenium
- script prod to dev uninstall module after installation
- adapt todo with private directory
- script to download remote database
- TODO show documentation for migration
- oficially support odoo 18 instead of odoo 16
- support postgis/postgresql 18 into docker
- change version erplibre 1.6.0
- support private environment and support local git repo manifest
- support switch odoo version
- support docker for each version
- update os installation
- upgrade python requirement
- separate virtual environment for erplibre and odoo