| name | odoo-upgrade-planner |
| description | Plan an Odoo module version upgrade (e.g. 16.0 to 19.0, 17.0 to 19.0) by first checking the Odoo Apps Store for a free ready-made replacement, then auditing the manifest, models, views, data records (cron/server actions), security, SCSS/CSS and JS on two axes — what changed upstream in the real odoo/odoo and odoo/enterprise repos, and where the module's own code still uses syntax the target version rejects (tree vs list, attrs, cron numbercall) as specified by the odoo-module-builder skill. Traces renamed/moved code via commit history and OCA/OpenUpgrade rather than assuming a 404 means deleted, verifies business-logic equivalence not just name matching, and flags every data-affecting rename with the migration script it needs. Writes one detailed task file per module; makes no code changes. Use when the user asks to plan/audit an Odoo module upgrade, invokes /upgrade-plan, or reports upgrade errors like "view not found", "field does not exist", "invalid inherit_id", or deprecated field errors and wants a plan before anything is touched. Pair with the odoo-upgrade-executer skill to actually implement the resulting task files. |
Odoo Upgrade Planner (/upgrade-plan)
You are auditing one or more existing Odoo modules ahead of a version
upgrade (16.0/17.0/18.0 → 19.0 by default). This skill's job stops at
producing a precise, per-module set of task files — it never edits
module code. Implementation is a separate skill, odoo-upgrade-executer,
which consumes what you produce here.
The two axes — audit both, every time
| Axis | Question | Where it's covered |
|---|
| A. Upstream changed | Does the core model/field/view this module depends on still exist in the target version? | The GitHub lookups in Steps 2–8 |
| B. Your code is written the old way | Is the module's own code written the way the target version requires? | references/target-convention-audit.md, applied within every step |
Auditing only Axis A is the most common way an upgrade plan misses
breakage: nothing upstream was renamed, but the module ships a <tree>
root tag, an attrs="{...}" dict, or a cron with numbercall — and won't
install. Axis B is specified by the odoo-module-builder skill, whose
reference files encode correct target-version shape copied verbatim from
shipped Odoo core. Use it as the spec — see
references/target-convention-audit.md for the mapping and for the
verified deprecation list.
Ground rules
- Every claim that something "exists / doesn't exist / changed in 19.0"
must be verified against the real
odoo/odoo and odoo/enterprise
GitHub repos at the target branch this session — not asserted from
memory or from references/version-breaking-changes.md, which is only a
list of hypotheses to check, not facts.
- A model/field/view not found at its old path is a lead, not a
conclusion. Odoo regularly moves and renames code between modules
(in 19.0 even
odoo/models.py moved to odoo/orm/models.py). Trace it —
via commit history, Odoo's [MOV]/[REM]/[REF] commit-tag convention,
and OCA/OpenUpgrade — before logging something as removed. See
references/github-lookup.md §5.
- Match business logic, not just names. A same-sounding successor
model/field/view is not proof of equivalence — read what it actually
does in the target branch and compare to what the original did.
- Every rename that touches stored data is a data-safety issue, not
just a code edit. A renamed field/model/table needs a migration script
or the data is lost on upgrade — see
references/data-safety-migrations.md. Log the required migration
action alongside every such finding.
- Don't pad the plan. Log what breaks or changes behavior on the
target version. Style differences that still work are at most an
## Optional modernization note, never a blocking task row.
0. Interview the user before auditing anything
Do not start Step 1 until you have answered:
- Scope — which module(s) or path(s)? List them explicitly; if given a
directory of many addons, enumerate and confirm the list with the user
rather than silently assuming "all of them."
- Source version — confirm via each module's
__manifest__.py
version key, don't just take the user's word for it.
- Target version — confirm 19.0, or ask if different.
- Enterprise or Community? — grep each module's
depends for known
Enterprise module names and tell the user what you found. If any
Enterprise dependency exists, Enterprise repo access is required — see
references/github-lookup.md for the gh auth check, and stop to ask
the user to authenticate if needed before auditing those parts.
- Output location — confirm the task files should go in
tasks/ at
the root of the scanned scope (default — see
references/task-file-format.md), or a different path if they prefer.
- Is
odoo-module-builder available? Check whether the skill is
installed alongside this one. If not, tell the user Axis-B checks will
fall back to direct source lookups (slower, same rigor) and offer the
one-line install.
Step 1 — Check the Odoo Apps Store for a free replacement (per module)
Do this before spending any time on Steps 2–9. Check
https://apps.odoo.com/apps/modules/<target_version>/<module_technical_name>
(and a store search by functional purpose if the technical name doesn't
resolve) for a maintained, free equivalent at the target version. If one
exists, recommend replacing the module instead of upgrading it — full
method and how to tell free vs. paid in
references/store-replacement-check.md. Log the result in a ## Store Replacement Check section at the top of that module's task file.
- Free replacement found, no customizations on top: recommend replace,
skip Steps 2–9 for that module, note it in
00-INDEX.md.
- Free replacement found, but the module has custom fields/views/logic
layered on the original: still recommend adopting the replacement for
base functionality, but scope Steps 2–9 down to only the customization
layer (how it now depends on/inherits from the replacement).
- No suitable replacement: proceed with the full audit below.
Step 2 — Manifest, dependencies & module structure
version — must be re-prefixed to the target series
(19.0.x.y.z). Odoo reads the first two numbers to decide compatibility.
depends — for each listed module, confirm it still exists in the
target branch under that technical name. A dependency that was merged,
renamed, or removed upstream makes the module uninstallable, and this is
invisible until install time. Trace per ground rule 2 before concluding
a dependency is gone.
data load order — verify it still respects dependency order (groups →
ACL → rules → cron/actions → views → menus), per module-builder's
module-structure.md.
assets bundles — confirm the bundle names used still exist in the
target branch (web.assets_backend etc.) and that globbed paths still
resolve after any file moves.
- Any manifest key that no longer exists in the target version.
- Log under
## Manifest in the module's task file.
Step 3 — Models, fields & Python API
- Find every model file:
grep -rl "_name\s*=\|_inherit\s*=" --include=*.py <module_path>.
- For each model touched (defined or inherited), extract every field
name the module declares or references.
- Axis A — verify against target-version source
(
references/github-lookup.md):
- Does the model still exist under that
_name?
- Does each field the module reads/writes on a model it doesn't own
still exist under that name on that model in the target branch? (You
are auditing the module's assumptions about core/enterprise models —
not re-auditing fields the module itself defines on its own models.)
- If a model or field 404s, don't stop there — search by symbol
across the branch, check
[MOV]/[REM]/[REF] commits, and check
OCA/OpenUpgrade before concluding it was removed
(references/github-lookup.md §5).
- Once you find a plausible successor, read its implementation and
confirm it does the same job — don't accept name similarity alone.
- Follow every
related= chain hop by hop. A field declared
related='a_id.b_id.c_field' traverses several models, and the break is
often on the last hop — on a model the module never names directly.
Verify each hop exists in the target branch, not just the first.
grep -rn "related=" --include=*.py <module_path>.
Real failure this catches: related='...uom_id.category_id' →
KeyError: Field category_id ... does not exist, because the whole
uom.category model was replaced by a UoM tree
(references/verified-19-changes.md §3).
- Audit overridden core methods for signature drift. This catches
nothing else in this skill catches, and it does not fail at install
— the module loads fine and raises
TypeError later, often on a screen
that looks unrelated to the module.
- Find them:
grep -rnE "def (_search|search|search_read|search_fetch|read_group|_read_group|name_search|_name_search|name_get|create|write|unlink|copy|default_get|fields_get|_compute_display_name)\b" --include=*.py <module_path>
- For each hit, compare the override's signature to the current
target-branch definition in
odoo/orm/models.py (note: moved from
odoo/models.py — §8). Check for removed params, renamed params, and
newly params (anything after a ).
Step 4 — Views: inherit_id, xpath, and syntax conventions
- Axis A — find every
<record model="ir.ui.view"> with <field name="inherit_id" ref="..."/>; resolve each ref to module.xml_id
and check the view still exists under that id in the target branch.
- If not, search the branch for a view on the same model with a similar
purpose (views get renamed, split, merged) — same "trace it, don't
assume deleted" approach.
- For views that do still exist, verify every anchor, not just the
view id. A resolving
inherit_id proves nothing about the element
inside it. Check each xpath expr="..." target and each
position= anchor (name=, class=, field name) is actually
present in the current target-version parent view. Cross-reference
the Step 3 row if the field itself was renamed.
Real failure this catches: <group name="messaging"> was removed from
the res.users form, so an inheriting xpath dies with
Element ... cannot be located in parent view
(references/verified-19-changes.md §4). res.users,
res.partner and res.config.settings forms are reorganized often —
treat modules inheriting them as high risk.
- Axis B — check the module's own view XML against module-builder's
views-search-filters.md and form-views.md:
<tree> root tags that should be <list>.
attrs="{...}" / states= that must become direct
invisible=/readonly=/required= Python expressions.
- Chatter — any
<div class="oe_chatter"> block with hand-written
message_follower_ids / activity_ids / message_ids fields must
become a <chatter/> tag (verified change; mapping and attribute
table in references/target-convention-audit.md §4). Also grep for
inheriting views that xpath into oe_chatter — those break
separately and are Axis-A findings.
- Search-view filter/group-by shape.
- Record under
## Views.
Step 5 — Data records: cron, server actions, sequences, templates
Views are not the only XML that breaks. Audit every non-view record the
module ships in data/:
ir.cron — check against module-builder's cron-jobs.md.
numbercall and doall were removed (verified: present in 16.0's
ir_cron.py, absent in 19.0) — any data record still setting them
fails on install. Also confirm interval_number/interval_type are
both set and the model_id ref resolves in the target branch.
ir.actions.server — check against server-actions.md: field names
(group_ids vs older groups_id), state, model_id/binding_model_id
refs still resolving.
- Sequences, mail templates, QWeb report templates, other
data/
records — confirm every ref= / model= they point at still exists
in the target branch.
- Record under
## Data Records.
Step 6 — Security: ACL, groups, record rules
ir.model.access.csv — every row's model_id:id must resolve to a
model that still exists in the target version (a stale row referencing
a removed model is a hard install error), and every group_id:id must
still exist.
- Groups — check against module-builder's
security.md. Specifically:
res.groups.category_id was removed — any
<field name="category_id" ref="base.module_category_*"/> on a
res.groups record is an install-blocker
(ValueError: Invalid field 'category_id' in 'res.groups'). The
application category now lives on a res.groups.privilege record that
the group points at via privilege_id; full before/after in
references/verified-19-changes.md §1.
- Record rules — confirm
domain_force doesn't reference fields
renamed/removed per Step 3, and that model_id refs resolve.
- Record under
## Security.
Step 7 — SCSS/CSS review
- List every
.scss/.css file in the module's static/src/.
- Check any Odoo/Bootstrap CSS variable names and core selector/class
references against
addons/web/static/src/scss/ (and relevant core
module templates) in the target branch. An undefined Sass variable is a
hard build failure, not a cosmetic issue.
- Record under
## SCSS/CSS, citing the target-branch file that confirms
the variable/class is gone, renamed, or unchanged.
Step 8 — JS / OWL review (extra care for POS)
- List every
.js file in static/src/.
- For OWL components/patches: verify the extended/patched class still has
the same name, file location, and public method signatures in the
target branch.
- Point of Sale modules change structure heavily between versions —
verify the exact file/class in
addons/point_of_sale/static/src/...
(or enterprise POS addons) at the target branch before assuming a
patch/inherit target still resolves. Treat POS JS findings as "needs
decision" by default (see references/version-breaking-changes.md).
- Record under
## JS.
Step 9 — Final convention sweep
Run module-builder's checklist.md over the module as a last pass to catch
anything the per-area steps missed, and confirm every section of the task
file is filled in (including ## Unverifiable for anything you couldn't
check). Findings go into whichever section they belong to, not a separate
one.
Output: one task file per module
Write tasks/<NN>-<module_technical_name>.md for each module in scope
(e.g. tasks/01-sale_custom.md, tasks/02-pos_custom_payment.md), plus a
tasks/00-INDEX.md summarizing every module, its store-replacement result,
its finding counts by severity, and a recommended execution order (a module
whose fields another module depends on comes before the dependent module).
Full schema in references/task-file-format.md.
Two output rules — these are what make the plan executable:
-
Every task is a - [ ] checkbox heading so progress is greppable
and the executor can tick it off in place.
-
Write imperative actions, not observations. Each task's Action
block names the exact file, line, old value and new value, and pastes
any code to be written. The executor should not have to re-derive
anything you already worked out.
Not: "the _search override needs updating".
Instead: "In models/pos_order.py:32, replace the signature line with
def _search(self, domain, offset=0, limit=None, order=None, **kwargs):
and change the super() call to return super()._search(domain, offset=offset, limit=limit, order=order, **kwargs)".
If a task can't be reduced to imperative steps, that's the signal it is
Needs decision: YES — write the three-part briefing (what the code
does today / why it breaks / proposed + alternative) and let the
executor ask the user, rather than shipping a vague instruction.
Every task also carries a Verify by line — the concrete observable that
proves it worked. For runtime-break tasks this must name the screen to
open, because a clean install proves nothing about them.
Do not merge findings from different modules into one file — the executor
skill processes one module's task file at a time, and per-module files keep
review manageable for the client.
Hard rules
- Never assert "X exists/doesn't exist/changed" without having actually
looked it up this session. If you can't check (no
gh auth, no
Enterprise access, network blocked), say so and log it under that
module's ## Unverifiable section instead of guessing.
- Never conclude "removed" from a single not-found lookup. Trace it
per
references/github-lookup.md §5 first.
- Never audit only Axis A. Every step has an Axis-B counterpart; a
module can be fully compatible with current core models and still fail to
install because of its own stale syntax.
- Never log a rename without its data-safety action. Every Models-row
rename gets a migration-script note per
references/data-safety-migrations.md, even when the code-level rename
itself looks mechanical.
- Make no code changes. This skill only reads and writes task files.
If the user asks you to also fix something while planning, tell them
that's the
odoo-upgrade-executer skill's job and offer to hand off once
planning is complete.
- Don't skip steps or reorder them — later steps' findings (e.g. a view
xpath fix in Step 4) often reference an earlier finding (a renamed field
in Step 3), and rows should cross-reference by id (
M1, V1, ...).
Reference files
| Need | File |
|---|
| Checking the Odoo Apps Store for a free replacement (Step 1) | references/store-replacement-check.md |
| Confirmed 19.0 changes with both-branch evidence — cite directly, no re-verification needed | references/verified-19-changes.md |
Axis B: using odoo-module-builder as the target-version spec | references/target-convention-audit.md |
How to check odoo/odoo + odoo/enterprise on GitHub, gh auth flow, tracing renamed/moved/deleted code | references/github-lookup.md |
Data safety: migration scripts required for renames (openupgradelib, core migrations/ convention) | references/data-safety-migrations.md |
Exact per-module task file schema (tasks/NN-module.md, 00-INDEX.md) | references/task-file-format.md |
| Known breaking-change patterns by version — hypotheses to verify, not facts | references/version-breaking-changes.md |