Third COW incident of this migration, and the first two tools did not cover
it. A copy freezes the module view it came from; version after version the
MODULE view is modernised and the copy is not, until a child xpath lands on an
anchor the copy never had:
Element '<xpath expr="//head/script[@id='web.layout.odooscript']">'
cannot be located in parent view
On 14.0 -> 15.0 that was web.layout, copied in the 12.0 era: no id on the
script tag, QWeb still saying t-raw. It had survived only because the 14.0
module xpath carried a fallback — //head/script[@id='…'] | //head/script
[last()] — that 15.0 removed. The breakage was years old; 15.0 merely stopped
hiding it.
The test is DIFFERENTIAL, and it has to be. Resolving a child's xpath against
its parent's own arch proves nothing: Odoo resolves against the COMBINED arch
of the whole chain, so a child of website.layout legitimately targets //header
coming from an ancestor. Measured on the real database, that naive rule
reported 5 copies where only 1 had a problem. Resolving twice — against the
module twin and against the copy — and keeping only what the twin satisfies
and the copy cannot, isolates drift and nothing else.
Two accuracy fixes the real data forced:
· inactive children are skipped; Odoo never applies them
· a missing lxml is now a LOUD failure. The first version fell back to
« everything resolves », so the checker answered « all clean » while
checking nothing — the worst possible outcome for a checker. Verified: the
bare system python3 has no lxml, .venv.erplibre does.
--reset copies the module arch over the copy, after saving the previous arch
to a timestamped file and printing the diff, because a copy can hold a real
customisation buried in the drift — on web.layout it was one <meta viewport>
among six differences.
Timing matters: drift only exists once the module views carry the new version,
so run this AFTER OpenUpgrade and BEFORE update_addons_all. Run on the 14.0
database it finds nothing about web.layout, and rightly so — there the module
view says t-raw too.
Verified end to end on a scratch database rebuilt from the real arches:
detection of the exact reported failure, dry-run changing nothing, --apply
resetting it, the backup holding the previous arch, and a clean re-check.
Then across the four migration databases, where it flags two latent drifts
(website_sale.product, website_blog.blog_post_complete) that no version bump
has surfaced yet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 13.0 -> 14.0 data migration died on:
duplicate key value violates unique constraint "res_groups_name_uniq"
Key (category_id, name)=(9, Allow to define fiscal years of more or less
than a year) already exists
In 13.0 that security group is declared by the core « account » module, so the
database holds it as account.group_fiscal_year. In 14.0 core account no longer
declares it and om_account_accountant (odoomates) does. Owning no XML id of its
own, the module tries to CREATE the group and collides with the existing row.
Renaming the XML id makes Odoo UPDATE the existing record instead. The record
id is untouched, so user assignments, access rights and record rules pointing
at the group survive -- on the reference database it holds none, but the fix
must not depend on that.
The fix hook also learns a « .sql » flavour, run through psql. The « .py »
flavour is piped into « odoo<target>-bin shell », which requires loading a
not-yet-migrated database with the TARGET version's registry -- precisely what
is failing at that point. A pure SQL fix needs no ORM and no registry.
Verified end to end on a throwaway copy of the stuck database: the statement
renames one row, a second run changes nothing, and the full 13->14 OpenUpgrade
then completes (« Modules loaded. », base at 14.0.1.3, zero
res_groups_name_uniq error).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
key is the only thing pairing a website copy with the module view it came
from. A copy whose key matches nothing is never paired, so it never receives
the new inherit_id, never changes shape, and never joins a view combination.
Renaming the key is therefore enough to take it out of the way:
UPDATE ir_ui_view SET key = '<prefix>.' || key, active = false
active = false alone would NOT work: an inactive copy keeping the same key
still shadows the module view. Nothing is deleted either, so inherit_id
ondelete='restrict' and the website_page foreign keys are never touched, and
the old arch stays in database as a readable archive. --restore undoes it.
Written in plain psql on purpose: it runs on a database not yet migrated,
where starting an Odoo shell of the target version is not guaranteed. It is a
systematic step driven by the detector, valid for every bump, so it lives in
the loop rather than in a per-version fix_migration file.
Offered rather than forced, since those copies carry real customizations.
Verified on a throwaway copy: dry-run writes nothing; apply renames the key and
deactivates while keeping the 4058-char arch; a second apply is a no-op;
restore returns the row to its exact initial state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A version bump rewrites website views without announcing any of it, so "the
site looks wrong" after a migration is currently unanswerable.
snapshot_cow_views.py records every website_id view (key, mode, inherit_id,
active, arch and its md5) and diffs two snapshots. Rows come back as JSON
straight from Postgres because an arch holds newlines and pipes; the column
list is intersected with information_schema, since ir_ui_view does not expose
the same columns from 12.0 to 18.0. Snapshots hold customer template content,
so they go under private/ and stay out of git.
todo_upgrade.py takes one before and one after each OpenUpgrade run, then
prints the diff. Both are non-blocking: forensic material must never stop an
upgrade.
Measured on the real 12.0 -> 13.0 jump, the diff shows what no log reported:
71 -> 68 copies, 16 deleted and 13 created. portal.frontend_layout is not
converted but DELETED (id 2670) and RECREATED (id 3397), with its children
re-parented from one to the other. The four theme_technolibre copies,
muk_web_branding, project_agile and erplibre_website_snippets_basic_html are
dropped outright, and website_crm.contactus_thanks is renamed to
website_form.contactus_thanks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two blind spots made the detector miss real breakages.
1. Comparing « mode » is not the right test. What decides the shape an arch
must have is whether the target declares an inherit_id: with one, the arch
must be inheritance specs (<data>, <xpath>, position=); without one, it must
be a standalone template. A view moving from a root template to
« inherit_id + primary="True" » keeps mode='primary' on BOTH sides, so the
old test reported nothing while the copy still broke. The comparison is now
(target declares inherit) vs (stored arch is spec-shaped), and each finding
carries the reason. A mode change with a matching shape is still reported,
as a lesser warning.
arch_db is read as text up to 15.0 and as jsonb from 16.0; both are handled.
2. A module renamed upstream was reported as « module absent », hiding every
view it owns. renamed_modules is now read from the target OpenUpgrade
apriori.py (21 entries for 13.0, 56 for 14.0, 39 for 16.0, 20 for 18.0) and
used before concluding the module is gone.
Also correct the advice printed for a copy at risk: deactivating it is not
enough, an inactive copy keeping the same key still shadows the module view.
Renaming the key is what actually unpairs it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A copy-on-write view freezes the structure of the module view it was copied
from. When that module view changes mode between two versions, the copy keeps
an arch written for the old mode and the upgrade dies on
« Element ... cannot be located in parent view », hours into the migration.
Measured on a real 12.0 database: portal.frontend_layout is declared primary in
12.0 (full QWeb template, so a full-template arch is legitimate) and becomes an
extension in 13.0 (inherit_id + xpath). The 2021 COW copy follows the module,
turns into an extension, and keeps its 12.0 arch. The customization is genuine;
the breakage is produced by the migration.
check_cow_views.py compares the mode stored in database with the mode declared
in the target version sources and sorts the views in three buckets: at risk
(mode change), module absent from the target version, and pages made from the
editor (not at risk). Read-only, and called from step 2 so the list is known
before the long version loop rather than during it.
On the reference database it reports exactly the view that broke the upgrade,
plus 7 views whose module is gone in 13.0 (custom theme included).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Which modules must be uninstalled before a version bump depends on the data of
one specific database, so that list does not belong to a shared versioned file.
- Read private/odoo/migration/<database>/uninstall_module_list_odooXX0_to_odooYY0.txt
first, then the shared versioned defaults under script/odoo/migration/, and
merge them (duplicates dropped). private/ mirrors script/, the convention
already used by script/todo/todo.json -> private/todo/todo_override.json.
- Fix the parser. It was « f.readline().split() »: only the FIRST line was kept,
so a multi-line list was silently truncated, and a comma-separated list turned
into one bogus module name. On a file starting with a comment it returned the
words of that comment as module names. It now reads every line and accepts
commas, several names per line, blank lines and comments.
- Support a « # reason » justification per module, printed before uninstalling;
a module with no reason is flagged. Removing a module must stay reviewable.
- Ignore private/odoo/ in git: these lists describe one database.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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