Commit graph

23 commits

Author SHA1 Message Date
03ed3a6a87 [ADD] migration: a read-only statistics screen, and a way out
The interface choice offered two ways to ACT on a migration and none to just
look at one. Entry [3] reads the progression file and the traces left under
private/, writes nothing, and can be opened as often as one likes — [0] there
returns to the interface choice, and a new [0] Cancel leaves the tool entirely
instead of forcing a migration to be started.

« stats » is deliberately not storable as a default: it does nothing, so
landing on it every time would only be in the way.

The dashboard answers what was asked — which modules were removed, and how far
between Odoo versions — plus what the recorded data already knew and nobody
was showing:

  · elapsed time since the migration started
  · module count per version, with the delta per bump — a jump losing 15
    modules does not mean the same thing as one losing none
  · modules reported missing or duplicated
  · which fix hooks exist and which have run
  · COW snapshots taken, with their view count over time
  · how many commands ran, and the annotated decisions among them

Then, on demand: the removed modules with their justification, the same list
comma-separated to paste, a diff between any two COW snapshots (through the
existing snapshot_cow_views.py --diff), the decisions, and the last commands.

Computation lives in migration_stats.py as pure functions fed by
resume_context() — the same context the resume screen and its TUI already
render, so the step and version-bump state can never be described two
different ways. read_uninstall is INJECTED rather than reimplemented: it
resolves private-then-global itself, so the screen shows exactly the list that
would be applied.

Verified on a realistic progression: 1 j 03 h elapsed, 312 -> 297 -> 282
modules (-15 each), 5 removals across two bumps with their reasons, 4 COW
snapshots ordered by time, the fix applied for 14.0 and pending for 15.0, and
the progression dict byte-identical afterwards. The menu was checked to map
'' /1/2/3/0/9 to tui/tui/cli/stats/None/tui, and [0] to leave without asking
anything else.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-02 03:16:50 -04:00
7d96b56fb4 [FIX] migration: tell modules with no code from those to uninstall
Uninstalling before the 13->14 bump asked for 15 modules; 12 of them no longer
exist in the 13.0 addons path. check_addons_exist.py rejects the command on
the first missing name, so the uninstall aborted whole — and the 3 modules
that WERE there stayed installed, blocking the bump for a reason that had
nothing to do with them.

The list is now split before the script is called, using check_addons_exist
which was already there and simply never consulted at this point. « Missing »
means the addons path has no code for it, not that the database lacks it —
the distinction is the whole point, since Odoo cannot uninstall a module whose
code is gone.

When some are missing, a choice is offered rather than a failure:

  [1] uninstall the present ones, skip the missing   (default)
  [2] try the whole list anyway (it will fail)
  [3] uninstall nothing, continue

Only what is really uninstalled is subtracted from the per-version module
list, so a module left in place stays counted as installed — which it is. The
missing names are written to the progression comments, so what was skipped is
still on record afterwards.

Verified on the exact list from the failure: 3 present and 12 missing,
option 1 issuing a command with only the 3 and leaving the 12 counted as
installed, option 2 sending all 15, option 3 running nothing.

Also swapped entries 4 and 5 of the Deploy menu — QEMU/KVM now sits at [4],
NTFY at [5].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 07:16:26 -04:00
d40e6aa011 [ADD] migration: a TUI resume screen, same first question as the prompt
The migration now opens with the same choice the QEMU deployment does — TUI
or line-by-line prompts — settled in advance by TODO > Configuration if you
want it to be. Choosing the TUI loads the saved progression and asks the very
same first question: where does this migration stand, and where do we resume?

prompt_resume was one function that rendered, read and decided at once, so a
second view would have meant a second copy of the decision. It is now three:

  resume_context()      the progression -> plain data (file, database, target,
                        steps with their icon and detail, version bumps)
  print_resume()        renders it on the terminal
  apply_resume_answer() answer -> (progression, changed)

The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4,
4.<version> — so apply_resume_answer stays the only place that decides what a
choice means. Neither view can drift into describing the migration
differently, because both render the same context.

In the TUI the steps are a table and the version bumps a list: Enter on either
replays from there, which is what « [0-4] » and « [4.N] » meant in text. The
cursor opens on the first unfinished step and on the first unmigrated version
— where it stopped is where one usually wants to act.

Both views gain « q », quit without doing anything. A TUI needs Escape to do
something sane, and an escape hatch present in only one of the two views is
exactly the kind of divergence this split exists to prevent. execute_odoo_
upgrade returns immediately on it, writing nothing.

Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps
migrated: identical context feeding both views, Enter on step 2 giving « 2 »,
Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every
answer producing the same progression through both paths — « 2 » leaving only
the state of steps 0 and 1, « 4.15 » resetting the clone list from the third
bump on so the half-migrated intermediate database gets rebuilt. « q » checked
to leave the progression file byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 06:38:31 -04:00
11d9b88de9 [IMP] migration: replay a single version bump, and stop offering a doomed retry
When OpenUpgrade failed, todo_upgrade_execute offered the generic
« [1] to redo the command ». Replaying it re-ran OpenUpgrade on the database it
had just half migrated, which never recovers. That call now passes
wait_at_error=False, and the failure message explains that the clone step has
been reset so a relaunch DROPS and REBUILDS the intermediate database from the
previous version.

The resume menu gains « [4.N] »: replay the step 4 loop from one version bump.
Only the per-version lists are trimmed from that index, so earlier bumps stay
migrated and steps 0 to 3 are untouched. Resetting the clone entry is the whole
point: the intermediate database of a failed bump must be rebuilt, not upgraded
again. « [4] » still replays every bump.

The per-bump uninstall files were also read at step 1 only, for the source
version, so uninstall_module_list_odoo130_to_odoo140.txt existed in name but
was never read. The step 4 loop now reads the file of the bump it is about to
perform, merged with the answers already stored in the progression.

Verified against the real progression (13 and 14 migrated): the menu offers
13/14/15/16/17/18, and replaying from 14 turns every per-version list from
[True, True, False...] into [True, False, ...] while state_4_reach_open_upgrade
and steps 0-3 survive. A 13->14 list of 4 modules is read with its reasons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 04:46:17 -04:00
1f0b5c021f [FIX] migration 13->14: hand group_fiscal_year over to om_account_accountant
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>
2026-08-01 04:21:06 -04:00
a9ef5a0e8e [IMP] migration: show the real state of each step and replay from any of them
The resume menu exposed internal key names: « Reuse database without state_4 »
tells nobody what will happen, and the most useful option -- keep the prepared
database, redo the version bumps -- was the least readable of the four.

It now prints where the migration actually stands: the zip, the database, the
target, and one line per step with its state. Step 4 reports how many version
bumps are migrated and names them, so « 0/6 · 13 14 15 16 17 18 » replaces a
list index nobody could interpret.

The four numbered options become:
  [c]   continue where it stopped        (was: press enter)
  [0-4] replay from that step            (new: rewind to any step)
  [n]   new migration, erase everything  (was: [1])
  [r]   keep the zip, ask everything     (was: [4])

Rewinding drops « state_* » from the chosen step onward and keeps the rest:
config_* answers, the zip and the target version are decisions, not progress,
so they are no longer re-asked. state_0_search_missing_module is still forced
back to False, as the previous code did: it fills an in-memory dict the later
steps rely on. Old [2] is now « replay from 0 », old [3] « replay from 4 ».

First use of t() in this file; the new strings are translated in todo_i18n.py.

Verified against the real progression of the 12.0 -> 18.0 migration and a
synthetic one: for every step, the kept keys are exactly those of the earlier
steps, config and target survive, and the module search is forced again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:53:17 -04:00
79a07ac577 [FIX] migration: rebuild the clone when the data migration fails
When OpenUpgrade fails, the intermediate <db>_upgrade_<N> database is left
half migrated. The clone step had already been recorded as done, so a rerun
skipped it and restarted OpenUpgrade on top of that broken clone.

Clear the clone flag on failure so the replay rebuilds the intermediate
database from the pristine source. Found by a real 12.0 -> 13.0 run: Ubuntu
unattended-upgrades restarted the PostgreSQL cluster mid-migration, Odoo lost
its connection, and the replay would have resumed on the damaged clone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 02:38:54 -04:00
8ce4b93292 [ADD] migration: neutralize the COW views that would break a version bump
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>
2026-08-01 01:04:38 -04:00
1e4cc4f471 [ADD] migration: snapshot and diff the website COW views at each version bump
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>
2026-08-01 00:58:54 -04:00
045ccc1c85 [ADD] migration: predict the website COW views a version bump will break
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>
2026-07-31 23:28:54 -04:00
4536c5bc40 [IMP] migration: per-database uninstall list, with a stated reason
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>
2026-07-31 23:25:43 -04:00
ab9f94cc28 [FIX] migration: stop recording failed steps as successful
The database migration recorded steps as done even when they had failed, so a
resume skipped them and the loop kept going on a broken database.

execute.py
- exec_command_live() left exit_code = None when an exception was raised. None
  is falsy, so « if not status: » marked the step done and
  « if status and wait_at_error » skipped the error prompt: a crashed command
  was reported as a success. Both except blocks now set exit_code = 1.
- The « no Odoo version installed » path returned a bare -1 while callers
  unpack a tuple (status, cmd), raising ValueError instead of surfacing the
  failure. It now returns the same shape the caller asked for.

todo_upgrade.py
- todo_upgrade_execute(): treat a None status as a failure (defence in depth).
- Database migration (OpenUpgrade): the return code was discarded, with an
  explicit « TODO detect error », and the state was written unconditionally.
  It is now captured; on failure the loop stops instead of migrating the next
  version on top of a half-migrated database.
- Neutralization: the flag was set before the « if not status » test, making
  that test dead code. Removed the unconditional assignment.
- Clone and fix-migration hook: their return codes were never captured, so the
  steps were marked done whatever happened. Both are now checked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 23:22:02 -04:00
d84d729c77 [UPD] script todo upgrade: ignore update all state 2 when done in state 1 2026-07-31 00:34:32 -04:00
b856548d64 [IMP] script todo: migration Odoo update all before neutralize 2026-07-30 23:20:21 -04:00
e9f14c2af9 [FIX] script todo upgrade: don't create branch if already exist 2026-06-29 03:22:58 -04:00
dd08281c6f FIX 2026-05-20 02:54:29 -04:00
4972add36f [FIX] todo upgrade wrong method get_odoo_version 2026-03-29 14:51:02 -04:00
1e731114f5 [IMP] script: update copyright year to 2026
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>
2026-03-11 23:16:05 -04:00
0a7bca4801 [UPD] script todo_upgrade support odoo --neutralize 2026-03-07 00:48:08 -05:00
54f8ba0feb [UPD] script execute to share exec_command_live 2026-02-13 04:07:44 -05:00
9769bc83b5 [FIX] todo upgrade: copying database when missing 2026-01-05 07:52:30 -05:00
41fbc39c21 [FIX] script config: support merge dict configuration between 3 config
- fix upgrade when missing log
2025-11-11 00:07:17 -05:00
cbc43fde3c [IMP] todo support odoo upgrade
- 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
2025-10-31 01:43:26 -04:00