Agent skill: migrate Odoo 14–19 modules to Odoo 20 and prove they install
Overview
is an agent skill that migrates a custom Odoo module (or a whole addons folder) from , then proves the result . It installs the module on a throwaway database and clicks through it in a real browser; getting the code to compile is not where it stops. It works with any coding agent that can read files and run shell commands. - The scanner knows 100+ breaking changes, and Odoo 20 breaks some code without an error: server t-esc renders nothing, _sql_constraints are dropped, name_get is never called, oe_chatter disappears, and overrides with an old signature crash only when called. The scanner counts these as seriously as install errors. - The module is copied into a separate Odoo 20 tree, with one git commit per phase, so every change is a diff you can review. Odoo core is checked for changes after every automatic pass. - A renamed copy of an Odoo module is rebuilt from the upstream Odoo 20 code, keeping your model names and only your real changes.
README
odoo-migrate-to-20 is an agent skill that migrates a custom Odoo module (or a whole addons folder) from Odoo 14, 15, 16, 17, 18 or 19 to Odoo 20, then proves the result installs and runs. It installs the module on a throwaway database and clicks through it in a real browser; getting the code to compile is not where it stops. It works with any coding agent that can read files and run shell commands.
- Nothing silent gets through. The scanner knows 100+ breaking changes, and Odoo 20 breaks some
code without an error: server
t-escrenders nothing,_sql_constraintsare dropped,name_getis never called,oe_chatterdisappears, and overrides with an old signature crash only when called. The scanner counts these as seriously as install errors. - Your original is never touched. The module is copied into a separate Odoo 20 tree, with one git commit per phase, so every change is a diff you can review. Odoo core is checked for changes after every automatic pass.
- Forks are rebased, not ported. A renamed copy of an Odoo module is rebuilt from the upstream Odoo 20 code, keeping your model names and only your real changes.
Install in 3 steps
1. Install the skill. Run this, then pick your coding agent when it asks:
npx skills add tanim913/odoo-migrate-to-20 -g
No Node.js? Use the included installer instead:
git clone https://github.com/tanim913/odoo-migrate-to-20.git
cd odoo-migrate-to-20
./install.sh --for all --project ~/work/my-odoo-repo # or e.g. --for cursor,claude
install.sh also adds the /odoo-migrate-to-20 command to Cursor, Antigravity, Copilot and
Gemini CLI.
2. Check your machine. Run this once; it must end with READY:
~/.agents/skills/odoo-migrate-to-20/scripts/doctor.sh
If it says NOT READY, its FAIL lines tell you what is missing: usually an Odoo 20 checkout, Python 3.12+ or PostgreSQL 16+ (see Requirements).
3. Use it. In your coding agent, type:
/odoo-migrate-to-20 ~/work/odoo16/addons/my_module
or just ask: “migrate my_module from 16 to 20”.
Works in your agent
The skill is written in the open Agent Skills format: a SKILL.md with
scripts and references. install.sh keeps one canonical copy in
~/.agents/skills/odoo-migrate-to-20. Skill-aware agents link to it; tools that only understand
rules or commands get a small adapter that points to it. An update in one place reaches all of them.
| Agent | --for |
What gets installed | How you start it |
|---|---|---|---|
| Claude Code | claude |
~/.claude/skills/… (+ /.claude/skills with --project-skills) |
/odoo-migrate-to-20 … or ask |
| OpenAI Codex | codex |
~/.codex/skills/… |
ask “migrate X to Odoo 20” |
| Cursor | cursor |
.cursor/rules/odoo-migrate-to-20.mdc + .cursor/commands/odoo-migrate-to-20.md (user and project) |
/odoo-migrate-to-20 in Agent chat, or ask |
| Google Antigravity | antigravity |
/.antigravity/skills/odoo-migrate-to-20.md + /.agent/workflows/odoo-migrate-to-20.md |
the /odoo-migrate-to-20 workflow, or ask |
| Gemini CLI | gemini |
~/.gemini/commands/odoo-migrate-to-20.toml (+ project) |
/odoo-migrate-to-20 |
| GitHub Copilot | copilot |
~/.copilot/skills/… + /.github/prompts/odoo-migrate-to-20.prompt.md |
/odoo-migrate-to-20 in Copilot Chat (agent mode) |
| OpenCode and other skill readers | agents |
~/.agents/skills/odoo-migrate-to-20 (+ /.agents/skills with --project-skills) |
picked up automatically |
| Anything that reads AGENTS.md | agents-md |
a marked section in /AGENTS.md |
ask |
The route
Every stop has a gate, and the route doesn’t move on until the gate is met:
- Doctor (
doctor.sh) checks Odoo 20, Python 3.12+, PostgreSQL 16+ and Chrome. Migration starts only at READY, or when you accept what is missing (for example, no enterprise). - Prepare (
prepare.sh) copies the module to$MIG20_TARGET_ROOT//, detects the source version, fingerprints the original and commits a baseline.fork_check.pydecides whether the module is a renamed fork (see below). - upgrade_code (
run_upgrade_code.sh) runs Odoo 20’s own rewrite scripts safely: one script at a time, module only, never throughodoo-bin,owl3-migrationincluded. It fails if Odoo core or enterprise changed. - Scan (
scan.py) checks 100+ known 14→20 breaking changes and reports each hit withfile:line, sorted by severity: BLOCKER, SILENT, RUNTIME, WARN. Target: 0 BLOCKER, 0 SILENT. - Rewrite: renames that cross Python, XML and JS go first, then each area (Python and controllers, views and security, backend JS, website) is migrated against the references. Agents that can run parallel sub-agents use them for the JS and website areas; others do the areas one after another.
- Install loop (
install_test.sh) installs on a throwaway database (mig20_*) untilRESULT: CLEAN, runs the module’s own tests, then drops the database. - Smoke test (
smoke_test.py) runs Odoo’s clickbot over the module’s apps, opens every other menu action and its “new” form, and every website page as public and admin, in headless Chrome. Then a short flow tour of the module’s main flow proves the migrated code works, not just loads. Any browser console error fails the run, and the screenshots of a failure are handed to the agent. - Review and report: a code review of the whole diff (using Odoo’s
odoo-reviewskill when available), thenMIGRATION_20.mdwith the decisions, what was not migrated, security findings, data-migration notes and what to check by hand.
What it catches
| Area | Examples |
|---|---|
| Manifest | a version format that silently makes the module uninstallable, removed keys, removed or merged dependencies, missing files, external Python libraries |
| Python / ORM | odoo.osv removal, read_group, name_get, _sql_constraints, @api.model create, the access-check API, odoo.http submodules, renamed fields, override signatures against Odoo 20, methods that now require an identity check |
| Security | ir.model.access + ir.rule → ir.access, res.groups privileges, public-access review, leaked credentials in source |
| Views / QWeb | attrs/states, tree → list, kanban-box → card, chatter, search view schema, server t-esc/t-raw (render nothing in 20), t-call parameters, report layouts, mail templates, renamed xmlids |
| Backend JS | Owl 2 → Owl 3 (useProps, proxy, signal.ref, template this.), legacy widgets, three-argument patch, services, widget renames, asset bundles |
| Website | publicWidget → Interactions, jQuery removal, frontend RPC, snippet options → html_builder, website_sale template and route changes, portal entries |
| Styling | Bootstrap 4 → 5, Font Awesome → Odoo’s oi icon font |
| Tests and tours | Odoo 20’s strict tour schema (extra_trigger, position, edition, test: true…), old run helpers, jQuery-only selectors, tour.register, steps that relied on the pre-18 implicit click, phantom_js, QUnit |
Beyond regular expressions, scan.py:
- imports every
odoo.*import for real on Odoo 20, so moved helpers and renamed classes are caught. A failing import insidetry/except ImportErroris flagged as silent, because on 20 the fallback branch would always run; - resolves every JS import to a file in Odoo 20 and checks each imported name is exported. One missing file breaks the whole asset bundle;
- imports every third-party library with your Odoo 20 Python;
- compares every override of a core method with its Odoo 20 signature;
- checks every name used through an Odoo module (
fields.X,models.X,api.X, …) exists in Odoo 20, not only the import lines (fields.datetime,models.NewIdwere removed); - checks every external xmlid the module references against Odoo 20;
- finds methods defined twice in one class (a rename can make one override replace another),
_loginoverrides that dropsuper(), oldgroup_expandsignatures, computes that only assignself._origin, and views written under another module’s XML ID (which break-uof that module); - looks for secrets.
How it proves the result
- A real install, not a syntax check, on a throwaway PostgreSQL 16 database, repeated until
RESULT: CLEAN, with the module’s own tests. - A real browser. Odoo’s clickbot and the module’s website pages run in headless Chrome, and
one console error fails the run. Odoo skips browser tests without Chrome or
websocket-client;doctor.shwarns, andinstall_test.shreports it as a problem, so a skipped run is never mistaken for a pass. - A real flow. A short tour of the module’s main flow (create and save the main record, submit the website form…) proves the migrated widgets and interactions work. When a browser test fails, the failing step and the screenshot Odoo took at that moment go straight to the agent.
- Control runs. When a failure looks unrelated to your code, the same smoke test runs on the upstream or core module alone. A failure that reproduces there is recorded, not blamed on the migration.
- Proof of no side effects. The original module’s fingerprint is compared at the end, core
must have no git changes, and no
mig20_*database may be left behind.
Tested routes
| From | Module | Result |
|---|---|---|
| 18.0 | website pages, Owl backend widgets, a mail hook | 104 blocking, silent or runtime findings → clean scan, clean install, browser smoke test passed |
| 16.0 | a renamed fork of an Odoo module | detected as a fork, rebased on upstream 20, clean install, 12 of 12 upstream tests passed |
Requirements
- Linux or macOS (Windows: use WSL), with bash, git, rsync and python3.
- An Odoo 20 community checkout:
git clone -b 20.0 --depth 1 https://github.com/odoo/odoo. Enterprise is optional. - A Python 3.12+ virtualenv:
pip install -r odoo/requirements.txt websocket-client. - PostgreSQL 16+, with a role that can create databases.
- Chrome or Chromium for the browser smoke tests. Without it they are skipped, and the skill says so.
- A capable agentic model with a large context gives the best results; the workflow is long. The scripts do the heavy, deterministic work (scan, install, test), so results don’t depend on the model remembering Odoo APIs.
What you get
The migrated copy lands in $MIG20_TARGET_ROOT//, inside a git repo with one
commit per phase. The run ends with MIGRATION_20.md:
- the scan summary before and after;
- what
upgrade_codechanged and what was done by hand, per area; - decisions to confirm, such as access rights, icon mapping and removed dependencies;
- anything not migrated, with the reason;
- security findings and data-migration notes for existing databases;
- what to check by hand.
To try the module, add the project folder to your Odoo 20 addons_path.
Scripts
| Script | Purpose |
|---|---|
install.sh |
install the skill or its adapters for each agent |
scripts/doctor.sh |
resolve and save the configuration, check prerequisites |
scripts/prepare.sh |
copy the module, detect its version, commit a baseline, fingerprint the source |
scripts/fork_check.py |
detect a renamed fork of an Odoo or vendor module and its Odoo 20 counterpart |
scripts/rebase_fork.py |
rebuild a fork from the upstream Odoo 20 module, renamed, keeping model names |
scripts/run_upgrade_code.sh |
run Odoo’s upgrade scripts safely (per script, module only, core-untouched check) |
scripts/scan.py |
find breaking changes (--json, --area, --fix-tesc, --icons) |
scripts/install_test.sh |
install (and test) on a throwaway database, summarise problems and failure screenshots, drop the database (--tags re-runs one test) |
scripts/smoke_test.py |
generate a browser smoke-test module (clickbot, website pages, flow tours) |
scripts/xpath_check.py |
list every view locator Odoo 20 cannot apply, in one run, without installing the module (odoo-bin shell) |
scripts/registry_check.py |
after the install: overrides of hooks removed from Odoo 20 (real MRO) and fields whose compute/inverse/search method does not exist (odoo-bin shell) |
scripts/qweb_if_chain_check.py |
after the install: QWeb pages where an inherited view separated a t-else/t-elif from its t-if (the page fails only when opened; odoo-bin shell) |
Knowledge lives in references/: 15 files on the upgrade tool, manifest, Python/ORM, field
renames, controllers, views, QWeb/reports/mail, security, backend Owl 3, website interactions,
website builder, website_sale/portal, SCSS/icons, tests and tours, and lessons learned from real
migrations.
The scripts also work on their own, without an agent:
S=~/.agents/skills/odoo-migrate-to-20/scripts
python3 $S/scan.py path/to/module # every Odoo 20 break, with file:line
python3 $S/fork_check.py path/to/module # is it a renamed copy of an Odoo module?
$S/install_test.sh path/to/addons_dir my_module # install on a throwaway database, report errors
Contributing
Every migration should leave the skill better:
- Add each new kind of break to
references/lessons-learned.md, and where possible as ascan.pyrule or check. - Keep the references verified against the Odoo 20 source, and cite the file that proves each rule.
The README artwork is generated by assets/src/build_assets.py. It uses League Gothic and Barlow
Condensed (SIL Open Font License); see the script for where to get the fonts.
License
MIT © tanim913. Odoo is a trademark of Odoo S.A.; this project is independent and not affiliated with or endorsed by Odoo S.A.
Recommended Tools
Try a different keyword or remove a filter.
Install
npx skillfish add tanim913/odoo-migrate-to-20