From fcfbcd25ce160a67dd302f560f0171f2a0cde1c6 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 12 Aug 2026 01:28:50 -0400 Subject: [PATCH] [IMP] migration: say why Odoo changing its own template is our problem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- script/odoo/migration/cow_drift.py | 48 ++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/script/odoo/migration/cow_drift.py b/script/odoo/migration/cow_drift.py index a8c5e85..5972d94 100644 --- a/script/odoo/migration/cow_drift.py +++ b/script/odoo/migration/cow_drift.py @@ -273,12 +273,56 @@ def render_shape(finding): ) lines += [f" -> {shape}", ""] lines += [ - " A copy frozen in one shape cannot be applied in the other: Odoo", - " stops on « cannot be located in parent view ».", + " Odoo changing the shape of its own template is NOT the problem: on a", + " database without a copy, the module upgrade rewrites the view and", + " nothing breaks. It breaks here because a COPY exists and froze the", + " old shape — the copy follows the module and becomes an extension,", + " while still holding a standalone template. Odoo then applies that", + " template as an inheritance spec and stops on « cannot be located in", + " parent view ».", + "", ] + # Sans ceci, cette vue se lit comme « du code Odoo qui change », et l'on + # se demande pourquoi c'est notre affaire. Ce qui la rend nôtre est ce que + # la copie contient — la seule chose que neutraliser ferait perdre. + lines += _what_the_copy_holds(finding) return "\n".join(lines) +def _what_the_copy_holds(finding): + """Ce que la copie porte en propre, dit en une ou deux lignes. + + Relie les deux moitiés de la question : le mécanisme explique POURQUOI ça + casse, ceci dit CE QU'IL EN COÛTE. Une copie identique à sa jumelle ne + coûte rien, et le savoir change la décision. + """ + if finding["module_id"] is None: + return [ + " This copy has no module view of that name: it is a page made in", + " the website editor, and nothing else holds its content.", + ] + left = finding["module_arch"].splitlines() + right = finding["copy_arch"].splitlines() + diff = [ + line + for line in difflib.unified_diff(left, right, lineterm="", n=0) + if line[:1] in "+-" and not line.startswith(("+++", "---")) + ] + if not diff: + return [ + " This copy is IDENTICAL to the module view it shadows: it holds no", + " customization at all, so neutralizing it loses nothing.", + ] + n_plus = sum(1 for x in diff if x.startswith("+")) + n_minus = sum(1 for x in diff if x.startswith("-")) + return [ + f" This copy differs from the module view by +{n_plus}/-{n_minus}" + " line(s):", + " that is the customization, and all that neutralizing gives up.", + " Run without --shape to read it.", + ] + + def render_all(lst_finding, shape=False): """The whole report, one block per finding.""" if not lst_finding: