====== Odoo Multi-Version Migration: 11.0 → 18.0 (OpenUpgrade) ====== This page documents the automated migration chain used to bring the **luxy** production database from Odoo 11.0 all the way to 18.0 using [[https://github.com/OCA/OpenUpgrade|OCA OpenUpgrade]], one major version at a time. It covers prerequisites, project layout, how to run a stage, and every non-obvious fix that was needed along the way — so a future run (or a run against a different database on this same chain) doesn't have to rediscover them. ~~NOTOC~~ ===== 1. Overview ===== Odoo (and OpenUpgrade) only supports migrating **one major version at a time** — there is no direct 11.0 → 18.0 path. The chain implemented here is: 11.0 -> 12.0 -> 13.0 -> 14.0 -> 15.0 -> 16.0 -> 17.0 -> 18.0 Each hop: - Duplicates the previous stage's database (cheap, via ''CREATE DATABASE ... WITH TEMPLATE''). - Runs that version's pre-migration SQL fixes (only needed for the early, hand-written-SQL stages). - Runs the real OpenUpgrade module migration (''odoo -u all'') inside that version's own container. - Auto-recovers from a library of known, safe failure signatures (see [[#5_generic_auto-recovery_patterns|Generic auto-recovery patterns]]). - Applies any additional known, stage-specific fixes. - Cleans up modules left in a broken state, clears the compiled asset cache, and reports the final module state. All of this is orchestrated by a single script: **''run_upgrade_stage.sh''**. ===== 2. Prerequisites ===== * **Docker + Docker Compose** (v2, ''docker compose'' not ''docker-compose''). * **A single shared PostgreSQL instance** (this project uses ''postgres:14.5'' for every version — Odoo talks to it over the network, the server version doesn't need to match the Odoo version). * **Disk space**: each version's Docker image is ~1.3–2.3GB, growing with version. Budget **at least 5GB free** before starting a new stage; the full chain (11 through 18, all images built) needs tens of GB. See [[#8_disk_space_management|Disk space management]]. * **Per-version OpenUpgrade checkout** under ''/openupgrade'' — either the old per-module ''migrations/'' convention (11.0–13.0) or the centralized ''openupgrade_scripts/'' + ''openupgrade_framework'' convention (14.0+). These are normally provided by a sibling project's ''init.sh'' (clones ''OCA/OpenUpgrade'' at the matching branch). * **bash 5.x** on the host running ''run_upgrade_stage.sh'' (uses ''set -euo pipefail''; see the note about ''local var'' under ''set -u'' in [[#9_gotchas_worth_remembering|Gotchas worth remembering]]). * ''psql'' access to the shared postgres container from the host (script shells out via ''docker exec postgres psql''). ===== 3. Project Structure ===== oca_openupgrade/ ├── docker-compose.yaml # one service per Odoo version + shared postgres + nginx-proxy ├── run_upgrade_stage.sh # the master automation script (see below) ├── pre_migration_cleanup.sql # 11->12 pre-migration SQL fixes ├── post_migration_cleanup.sql # 11->12 post-migration SQL fixes (asset cache, etc.) ├── migration12to13.sql # 12->13 pre-migration SQL fixes ├── migration13to14_new.sql # 13->14 pre-migration SQL fixes ├── upgrade_logs/ # every stage's driver + per-attempt logs ├── 11.0/ … 18.0/ # per-version: │ ├── config/odoo.conf # Odoo config (addons_path, db creds, admin_passwd) │ ├── addons/ # extra/private addons for that version │ ├── openupgrade/ # OCA OpenUpgrade checkout for that version │ └── odooXX-data/ # bind-mounted /var/lib/odoo (filestore, sessions) └── fix_corrupted_view_translations.sql # standalone fixes, portable to any server └── cleanup_leftover_uninstalled_modules.py # (see section 7) ==== 3.1 docker-compose.yaml ==== * One service per version: ''odoo11'' … ''odoo18'' (odoo19 defined but not yet used). * A single ''postgres'' service shared by every version. * Only the versions currently being worked on need to be **uncommented** — once a stage is complete and verified, its container/image can be stopped and removed to save disk (see section 8). The corresponding block in ''docker-compose.yaml'' can be commented back out; the database itself lives in Postgres independently of the container. * ''odoo11'' uses the stock ''odoo:11.0'' image (no OpenUpgrade lib needed at that version's image level — OpenUpgrade there is just an addons-path checkout). ''odoo12''+ use a custom ''Dockerfile'' that installs ''openupgradelib'' on top of the base Odoo image. ==== 3.2 Per-version odoo.conf ==== Two config bugs recur across versions and are worth checking **every time a new version's container is brought up for the first time**: - **A stray space in ''addons_path''** (e.g. ''/mnt/openupgrade , /mnt/extra-addons''). Odoo's HTTP static-file bootstrap (''load_addons()'' / the ''Application.statics'' property in ''odoo/http.py'') does a raw, unguarded ''os.listdir()'' over every ''addons_path'' entry. A trailing-space entry that doesn't exist as a real directory throws an uncaught exception that **silently disables all static file serving for the life of the process** — the page loads with zero CSS/JS, and every static asset 404s, with no error in the Odoo log beyond the very first request. Fix: remove the stray space/comma so every entry is a real, listable directory. - The same class of bug: a **nonexistent** ''addons_path'' entry entirely (e.g. a leftover reference to a since-uninstalled OCA module's own repo path). Same symptom, same fix — remove the dead entry. docker exec -u root sed -i 's#/mnt/openupgrade ,#/mnt/openupgrade,#' /etc/odoo/odoo.conf docker restart ==== 3.3 run_upgrade_stage.sh ==== Usage: ./run_upgrade_stage.sh # Stages: 11to12 | 12to13 | 13to14 | 14to15 | 15to16 | 16to17 | 17to18 ./run_upgrade_stage.sh 11to12 luxy_prod luxy_11to12 ./run_upgrade_stage.sh 12to13 luxy_11to12 luxy_12to13 ./run_upgrade_stage.sh 13to14 luxy_12to13 luxy_13to14 ./run_upgrade_stage.sh 14to15 luxy_13to14 luxy_14to15 ./run_upgrade_stage.sh 15to16 luxy_14to15 luxy_15to16 ./run_upgrade_stage.sh 16to17 luxy_15to16 luxy_16to17 ./run_upgrade_stage.sh 17to18 luxy_16to17 luxy_17to18 Pipeline run by ''main()'' for every stage: - ''ensure_container_env'' — filestore ownership (''chown odoo:odoo /var/lib/odoo''), the ''odoo.openupgrade'' stub for 11–13 (see 4.1), and any stage-specific **container source patches** (see each stage's notes below). - ''recreate_db'' — terminates connections, ''DROP DATABASE'', ''CREATE DATABASE ... WITH TEMPLATE ''. - ''run_pre_sql'' — the stage's hand-written pre-migration SQL file, if any (only 11→12, 12→13, 13→14 have one). - ''apply_known_fixes_'' — proactive, stage-specific SQL fixes applied **before** the migration starts (see each stage's section). - ''run_migration'' — the actual ''odoo -u all --stop-after-init'' run, in a retry loop (up to 60 attempts) that calls ''try_generic_recovery()'' after every failure (section 5). - On success: ''cleanup_stuck_modules'', ''cleanup_leftover_uninstalled_modules'', ''fix_corrupted_view_translations'' (sections 6–7), ''run_post_sql'', ''clear_asset_cache'', ''report_state''. ===== 4. Stage-by-Stage Notes ===== ==== 4.1 11 → 12 ==== * **addons_path ordering bug**: the 11/12 images run vanilla Odoo with the OpenUpgrade addon tree mounted alongside it. Odoo resolves each module name to a **single** winning ''addons_path'' directory (used for both code *and* migrations, no fallback). Community addons (repair, sale, stock, account, …) must resolve to ''/mnt/openupgrade/addons'' **first**; core modules (''base'', etc.) must resolve to ''/mnt/openupgrade/odoo/addons'' first, so the ''base'' module's own pre-migration (which does the module-rename/merge bookkeeping many other modules depend on) actually runs. * ''odoo.openupgrade'' **stub**: the ''base'' module's OpenUpgrade code imports ''odoo.openupgrade'' — a small pair of helper files that doesn't exist in vanilla ''dist-packages'' Odoo. Copied in once per container, idempotent. * **point_of_sale demo data crash**: ''point_of_sale/12.0.1.0.1/noupdate_changes.xml'' writes ''default_code'' on an existing demo product, triggering a ''KeyError'' deep in the computed-field registry. Fix: drop that one '''' line. * ''account_asset'' **vs** ''account_asset_management'' (OCA): mutually exclusive; force ''account_asset'' to ''uninstalled'' so the OCA migration can take over its tables. * **Premature renames reversed**: the original pre-migration SQL pre-emptively renamed ''hr_holidays(_status)'' and ''procurement_rule'' as a workaround for the addons_path bug above. Once that root cause was fixed, those premature renames had to be **reversed** so the real OpenUpgrade scripts could do the full transformation themselves. * **Space-corrupted xmlids**: a data-quality artifact (xml_id ''name'' containing a literal space) that breaks lookups the target version's data files expect by the correct (underlying) name. Generic fix, reused every stage after. ==== 4.2 12 → 13 ==== * **toggle_active anchor injection**: Odoo 13 replaced the old ''