Skip to main content
How to Upgrade Custom Odoo Modules to Odoo 20
14 min read
Odoo

How to Upgrade Custom Odoo Modules to Odoo 20

A practical developer guide to migrating custom Odoo modules to Odoo 20, including code review, upgrade scripts, Upgrade Utils, views, data, testing and deployment.

Upgrading a custom Odoo module to Odoo 20 is not a matter of changing the version in __manifest__.py until the module installs.

A reliable module migration has two separate goals:

  1. make the source code compatible with Odoo 20;
  2. preserve the historical customer data and business behavior that the old module created.

The safest workflow is therefore:

audit → clean Odoo 20 compatibility → data migration scripts → upgraded-database testing → business regression → rehearsal → production.

Odoo’s own customized-database guidance follows the same principle: first make custom modules installable and functional on an empty target-version database, then make them work correctly with the upgraded customer database.

This guide focuses on the module itself.

Developer note: The examples below illustrate migration patterns. Verify every API/helper against the current Odoo 20 documentation and test it on a disposable database before using it on customer data.

1. Start by deciding whether the module should still exist

Before porting code, challenge the customization.

Ask:

  • What business requirement does this module solve?
  • Is the requirement still active?
  • Does Odoo 20 now solve it natively?
  • Can configuration or Studio replace it?
  • Is another custom module duplicating the same behavior?
  • Does the maintenance cost still justify the customization?

This is not just project management.

Deleting 2,000 lines of obsolete customization is often a better Odoo 20 migration than successfully porting all 2,000 lines.

Create a module inventory such as:

ModulePurposeComplexityData migrationExternal integrationOdoo 20 native replacement?
custom_sales_targetSales targetsMediumYesNoReview
custom_mobile_apiMobile endpointsHighMaybeYesNo
legacy_view_patchUI workaroundLowNoNoPossibly

Only migrate what still creates value.

2. Create a migration branch and freeze the baseline

Start from the exact production revision.

A practical Git approach is:

bash
git checkout <production-branch>
git pull
git checkout -b migration/20.0

The exact branch strategy is your choice, but the important requirement is traceability.

You should know:

  • which Odoo 19 commit is currently in production;
  • which commit starts the Odoo 20 migration;
  • which fixes were added after the freeze;
  • which commit was used in each migration rehearsal.

Avoid migrating a moving target.

3. Update the module structure deliberately

Start with __manifest__.py.

Review:

  • version;
  • dependencies;
  • data files;
  • demo data;
  • assets;
  • external dependencies;
  • install hooks;
  • application flags.

Do not only change:

python
"version": "19.0.1.0.0"

to:

python
"version": "20.0.1.0.0"

and call the module migrated.

The manifest change should happen alongside a dependency review.

If the module depends on a standard module whose responsibility moved or changed in Odoo 20, that can affect everything downstream.

4. Use Odoo’s code-upgrade tooling as an accelerator, not an answer

Current Odoo developer documentation includes an upgrade_code CLI for best-effort source rewrites.

Its purpose is to handle some repetitive forward-port work across source files.

A documented pattern is:

bash
odoo-bin upgrade_code \
  --from 19.0 \
  --to 20.0 \
  --addons-path=/path/to/custom/addons \
  --dry-run

Start with --dry-run.

Review what Odoo proposes before allowing automated rewrites.

Odoo explicitly warns that these scripts are best effort and not silver bullets.

After any automated rewrite:

  • review the diff;
  • run lint/static checks;
  • install on clean Odoo 20;
  • run tests;
  • inspect warnings;
  • manually review business-sensitive code.

An automated source transformation cannot know whether your custom workflow is still correct.

5. Search for known Odoo 20 framework changes

The official ORM changelog is one of the first places to check before debugging module-by-module.

Current Odoo 20 entries include:

  • removal of Model._table_query;
  • BinaryValue.filename and a changed RPC representation for binary fields;
  • simplified translation manipulation;
  • a new populate script for test-data generation;
  • changes to storage of manifest translation terms.

Use those changes to create repository searches.

For example, search for _table_query across custom addons:

bash
grep -R "_table_query" /path/to/custom/addons

If a custom SQL/reporting model depends on it, treat that as an explicit migration task.

Do the same for any documented API that your modules use.

6. Review Python ORM code

For each model, inspect more than syntax.

Review:

  • create();
  • write();
  • unlink();
  • computed fields;
  • inverse methods;
  • constraints;
  • onchange behavior;
  • company-dependent logic;
  • recordset assumptions;
  • context usage;
  • sudo() usage;
  • direct SQL;
  • imports from internal/private modules.

A method can run without exceptions and still be wrong.

Example: business semantics matter

Suppose an Odoo 19 customization overrides write() to synchronize a field into another model.

After migration, ask:

  • Does the target field still mean the same thing?
  • Is the synchronization still necessary?
  • Does standard Odoo 20 now perform related behavior?
  • Can the override cause duplicate work?
  • Does it behave correctly on multi-record writes?

Migration review is an opportunity to remove fragile overrides rather than mechanically port them.

7. Review SQL-backed and reporting models carefully

If your module creates _auto = False models, custom SQL views or reporting models, give them their own migration checklist.

Odoo 20 removes Model._table_query.

That means any module depending on that mechanism needs redesign using the supported Odoo 20 approach.

Also validate:

  • table/view creation lifecycle;
  • SQL aliases;
  • field types;
  • joins;
  • company filters;
  • currencies;
  • grouping;
  • access rules;
  • report totals.

A reporting view that renders successfully can still return subtly wrong numbers.

8. Review XML views and inherited XPath expressions

View inheritance is a common migration failure point.

Check every inherited view for:

  • parent XML ID;
  • fields that moved or disappeared;
  • XPath targets;
  • groups;
  • modifiers;
  • actions;
  • buttons;
  • settings blocks;
  • list/form/search/kanban structure.

A fragile XPath such as:

xml
<xpath expr="//div[3]/div[2]" position="inside">

may have worked for years because the upstream structure stayed stable.

Prefer selectors tied to meaningful fields, names or stable attributes where possible.

Do not automatically restore every disabled view

During a standard upgrade, problematic custom views may be disabled.

Review the Upgrade Report and ask whether the view should actually return.

If Odoo 20 now provides the required workflow natively, deleting the old inherited view can be the correct migration.

9. Review JavaScript, Owl and asset bundles

Backend success does not prove frontend compatibility.

For modules that customize the web client, POS, website or portal, review:

  • imports;
  • registries;
  • services;
  • patches;
  • Owl components;
  • templates;
  • asset bundles;
  • RPC calls;
  • browser events;
  • public widgets/controllers where relevant.

Then inspect the browser console during testing.

A migration can look correct to a backend developer while a JavaScript error quietly disables a user workflow.

10. Review controllers and integration contracts

Custom controllers are contracts with other software.

Inventory:

  • routes;
  • authentication;
  • request/response shape;
  • binary payloads;
  • error codes;
  • CORS/security behavior;
  • API keys/tokens;
  • mobile clients;
  • webhooks.

Odoo 20’s ORM changelog changes the RPC representation of binary fields, so document-heavy integrations deserve explicit regression tests.

Do not assume an external client is safe because the Python module installs.

11. Review security as a migration task

Security regression belongs beside functional regression.

Test:

  • ir.model.access.csv;
  • record rules;
  • groups and implied groups;
  • company boundaries;
  • portal/public users;
  • sudo() paths;
  • server actions;
  • API users.

Create test users for representative roles.

A migration that exposes another company’s records is more serious than a broken layout.

12. Separate code migration from data migration

This distinction is critical.

Suppose Odoo 19 has:

python
legacy_code = fields.Char()

and the Odoo 20 module redesigns it as:

python
reference_code = fields.Char()

Changing the Python field name does not safely migrate historical references by itself.

Odoo’s official customized-database guidance says renamed technical data—models, fields and external identifiers—should be handled with upgrade scripts to avoid data loss.

13. Use upgrade scripts for historical data changes

Odoo upgrade scripts are Python files containing a migrate(cr, version) function.

Current documentation supports pre, post and end phases under a structure such as:

text
your_module/
└── upgrades/
    └── 20.0.2.0/
        ├── pre-10-rename.py
        ├── post-10-fill-data.py
        └── end-10-cleanup.py

The upgrades directory name is supported and preferred in current documentation because “migration” can be confused with moving away from Odoo.

Pre phase

Runs before the module is loaded.

Useful for structural operations that must happen before the new model definition is applied.

Post phase

Runs after the module and its dependencies are loaded/updated.

Useful when the new schema/ORM is needed.

End phase

Runs after all modules for that version have been loaded and updated.

Useful for work requiring the final upgraded registry/state.

Choose the phase based on dependencies—not personal preference.

14. Prefer Upgrade Utils for reference-aware renames

Odoo’s Upgrade Utils exist because technical names can be referenced in many places.

For a field rename, the documented helper is conceptually:

python
from odoo.upgrade import util

def migrate(cr, version):
    util.rename_field(
        cr,
        "your.model",
        "legacy_code",
        "reference_code",
    )

rename_field() can update references beyond the physical column, including items such as filters, exports, server actions, related fields and domains.

That is safer than thinking only in terms of:

sql
ALTER TABLE ...

Other useful patterns include:

python
util.rename_model(cr, "old.model", "new.model")

and:

python
util.rename_xmlid(
    cr,
    "your_module.old_identifier",
    "your_module.new_identifier",
)

Verify the exact current signatures before publishing or running production code.

15. Handle noupdate data explicitly

A frequent migration surprise is configuration stored in XML with noupdate.

Changing the XML file does not necessarily update the existing database record during a normal module update.

Odoo’s Upgrade Utils include update_record_from_xml() for intentionally refreshing a record from its XML definition.

Current documentation notes that this helper uses the ORM and therefore belongs in a phase where referenced models are loaded—typically post or end.

This can matter for:

  • scheduled actions;
  • server actions;
  • templates;
  • sequences;
  • rules;
  • configuration records.

Do not force-update every noupdate record. Identify which records actually need the new definition.

16. Make the module install on a clean Odoo 20 database

Before testing customer data:

  1. create a clean Odoo 20 database;
  2. install dependencies;
  3. install the custom module;
  4. fix errors and warnings;
  5. run automated tests;
  6. execute representative workflows.

This separates source compatibility from historical-data migration.

If the module cannot work on clean Odoo 20, an upgraded customer database will only make debugging harder.

17. Then test against an upgraded customer database

Once clean-target compatibility is stable, combine the module with a test-upgraded customer database.

Now look for data-specific problems:

  • missing values;
  • renamed fields;
  • stale selection values;
  • broken XML IDs;
  • duplicate records;
  • disabled views;
  • invalid domains;
  • old configuration;
  • archived records;
  • multi-company inconsistencies;
  • orphaned references.

This is where upgrade scripts prove whether they are correct.

18. Test the module by business effect

Do not stop at “all tests green.”

Map each customization to the outcome it is supposed to create.

CustomizationTechnical checkBusiness check
Sales field/computevalue computesquotation total/workflow is correct
CRM automationaction executescorrect owner/stage/activity
Inventory hookcode runsreservation/move/valuation is correct
MRP customizationMO completescomponents/subassemblies/cost are correct
Accounting logicmove postsaccounts/taxes/analytics are correct
APIresponse is 200client receives correct semantics
ReportPDF rendersvalues/layout/translations are correct

This is especially important for stock, MRP and accounting modules, where a technically successful transaction can still produce a wrong downstream result.

19. Add migration-specific automated tests

Existing functional tests are useful, but migration introduces new failure modes.

Add tests such as:

  • old field values survive a rename;
  • renamed XML IDs resolve;
  • historical records preserve company ownership;
  • old workflow states remain valid;
  • noupdate configuration reaches the intended target state;
  • security still blocks cross-company access;
  • reports match known fixtures;
  • integrations preserve expected payload semantics.

The goal is to make the migration reproducible, not dependent on one developer remembering every manual check.

20. Use generated test data where it helps

Odoo 20’s ORM changelog introduces a new populate script for fake test data.

For performance-sensitive modules, generated data can help test:

  • search performance;
  • computed fields;
  • batch operations;
  • reports;
  • cron jobs;
  • list views;
  • large recordsets.

This is particularly useful when copying real production data into a development environment would create privacy or security concerns.

Generated data does not replace upgraded production-like UAT, but it improves engineering coverage.

21. Rehearse the module upgrade from a fresh database copy

Do not use the same database developers have been manually fixing for weeks as proof that production will work.

Take a fresh source database/test upgrade and repeat:

  1. deploy the tested Odoo 20 code;
  2. update the module;
  3. run pre/post/end upgrade scripts;
  4. inspect logs;
  5. run automated tests;
  6. run technical smoke tests;
  7. run representative business tests.

Record every manual command.

If a developer has to “remember to fix this row manually,” convert that knowledge into an upgrade script or a documented, repeatable cutover step.

22. Keep upgrade scripts idempotent where practical

An upgrade process is easier to debug when a failed rehearsal can be repeated predictably.

Where practical:

  • check whether the old object/value exists before renaming;
  • avoid creating duplicates;
  • log affected row counts;
  • fail loudly on impossible states;
  • avoid broad unbounded updates without validation;
  • keep destructive cleanup until the appropriate phase.

Do not over-engineer every one-off script, but design for safe rehearsals.

23. Review logs and warnings, not only exceptions

A module can install with warnings that indicate future problems.

Review:

  • deprecation warnings;
  • missing fields/views;
  • access warnings;
  • failed assets;
  • translation issues;
  • server-action errors;
  • cron failures;
  • browser console errors.

Treat a clean log as an engineering objective.

24. Production deployment checklist

Before upgrading the production module:

Code

  • Migration branch is reviewed.
  • Automated rewrite diff is reviewed if upgrade_code was used.
  • Module installs on clean Odoo 20.
  • Tests pass.
  • No unresolved critical warnings.
  • Security review completed.

Data

  • All renamed models/fields/XML IDs mapped.
  • Upgrade scripts tested from a fresh database.
  • noupdate records reviewed.
  • Disabled views reviewed.
  • Historical records validated.

Frontend/integrations

  • Views tested by role.
  • JS/Owl/browser console checked.
  • Reports validated.
  • API/controllers tested.
  • Binary/document workflows tested.

Rehearsal

  • Fresh rehearsal completed.
  • Runtime measured.
  • Manual steps documented.
  • Smoke tests defined.
  • Business owner accepted UAT.

Common mistakes when upgrading custom Odoo modules

Only changing the manifest version

That changes metadata, not compatibility.

Fixing errors directly on the upgraded database

Fix source code and write upgrade scripts so the process can be repeated.

Using SQL for every rename

A database column may have references elsewhere in Odoo. Use reference-aware Upgrade Utils when appropriate.

Putting every script in post

Odoo has pre, post and end phases for a reason. Choose based on when the old/new schema and registry are available.

Ignoring noupdate

Source XML changing does not guarantee the historical database record changes.

Trusting automated code rewrites blindly

upgrade_code is best effort. Review every meaningful diff.

Testing only fresh records

The difficult bugs usually live in historical data.

Forgetting frontend and integrations

Python success does not prove Owl, POS, mobile apps or API clients still work.

Migrating code Odoo 20 has made unnecessary

Delete technical debt when you can.

Frequently asked questions

Can Odoo automatically upgrade a custom module to version 20?

Odoo provides tooling that can assist with source rewrites and database upgrade operations, but current documentation explicitly describes code-upgrade scripts as best effort rather than a complete automatic migration. Custom behavior still requires review and testing.

Where should Odoo upgrade scripts go?

Current Odoo documentation supports module upgrade scripts under migrations/$version/ and also upgrades/$version/, with upgrades described as the preferred naming. Scripts use pre-, post- or end- filename prefixes to determine execution phase.

When do upgrade scripts run?

They run when the module is updated, subject to the module/version directory rules documented by Odoo. The pre phase runs before the module is loaded, post after the module/dependencies are loaded and updated, and end after all modules for that version are loaded.

How should I rename a custom field without losing data?

Use an upgrade script and consider Odoo’s rename_field() Upgrade Utils helper, which is designed to update the field and related references. Verify its current signature and test the migration on a disposable database first.

Should I test on a clean Odoo 20 database or the upgraded customer database?

Both. Clean-target testing isolates code compatibility. Testing on the upgraded customer database validates historical data and configuration. Odoo’s customized-upgrade guidance explicitly follows this sequence.

Final takeaway

A good Odoo 20 custom-module migration is not measured by how quickly the module installs.

It is measured by whether:

  • the source uses supported Odoo 20 patterns;
  • historical data survives intentionally;
  • references and configuration remain consistent;
  • security remains correct;
  • frontend and integrations still work;
  • the business result remains the same or improves;
  • the entire migration can be repeated from a fresh database without undocumented fixes.

The most useful migration mindset is:

automate the mechanical changes, engineer the semantic changes, and test the business outcome.


Planning an Odoo 20 project? Octivex Tech helps businesses with Odoo upgrades, custom module migration, integrations and AI-enabled workflows. Explore our Odoo services or contact our engineering team to discuss your environment.