Odoo version upgrades are not free. Anyone who tells you otherwise has not personally upgraded a real custom module across two major versions. The core platform moves fast: the ORM changes, the frontend framework changes, the asset pipeline changes, and the security model tightens. Standard Odoo handles those changes because the core team controls both sides of the equation. Your custom modules do not get that treatment. They break, and the breakage is yours to fix.
What follows is not a comprehensive upgrade checklist. It is the five categories of breakage we encounter on nearly every upgrade engagement, with the symptom pattern, the underlying cause, and the fix for each. If you know what to look for before you start, the work goes faster and surprises are fewer.
1. ORM API changes
The Odoo ORM (Object-Relational Mapper) is the layer your Python code uses to read, write, and compute on records. It is also one of the most actively maintained parts of the platform, which means it changes.
What the symptom looks like. The most common presentation is a TypeError or AttributeError at runtime, often inside a compute method or an override of create, write, or unlink. Sometimes the error is immediate on module install. Sometimes it only surfaces when a specific workflow runs.
The underlying change. Two patterns repeat across versions. First, method signatures on models.Model change: a keyword argument is renamed, a parameter that was optional becomes required, or a method that returned a single record now returns a recordset. Second, field computation dependencies tighten: a @api.depends decorator that used to work without listing a transitive dependency now silently produces stale computed values, or raises a warning that becomes an error in the next version.
The fix pattern. Before the upgrade, search your custom modules for every override of create, write, unlink, and copy, and every custom compute method. For each one, read the new version's source for the model you are inheriting. Diff the method signatures. The Odoo upgrade guide for each version ships a deprecation list; start there, but do not stop there, because not every breaking change makes the list. Pay particular attention to @api.depends chains on computed fields. If a computed field reads through a relational field, every level of that chain needs to be listed explicitly.
The conservative fix is always to make your dependency list more explicit, not less. A stale computed value is a data correctness bug, and those are harder to find than an explicit error.
2. QWeb and OWL frontend changes
Odoo's frontend has been through a significant architectural shift in recent major versions, from classic QWeb widgets toward the OWL (Odoo Web Library) component model. If your custom modules touch the UI, this category will find you.
What the symptom looks like. The page loads but a widget is missing. A custom button does nothing when clicked. The browser console shows a ComponentError or a failed component registration. Sometimes the form view renders entirely blank in the affected area.
The underlying change. The widget registry, component registration API, and the way field widgets are declared have all changed as Odoo has moved further into OWL. A widget written against the older class-based API (AbstractField, FieldWidget subclasses, use_field_node) does not automatically work in a version that expects an OWL component registered via registry.add. Similarly, custom QWeb templates that relied on deprecated t-on- event binding syntax or older helper utilities may silently fail or throw a runtime component error.
The fix pattern. Audit every custom JavaScript widget and field component before you start the upgrade. The question for each one is: which registration API does this use, and does the target Odoo version still support it? For anything written against the pre-OWL API, plan a rewrite as an OWL component, not a patch. Patching old-API widgets into new-API versions produces fragile code that breaks again at the next upgrade.
For server-rendered QWeb (reports, emails, website pages), the failure mode is different: a template extends a view that was renamed or restructured in the new version, and the inherit_id reference breaks at install. A project-wide search for inherit_id references to Odoo's own view XML IDs, cross-checked against the new version's view files, catches these before they become a production incident.
3. Deprecated fields and methods
Odoo deprecates fields and methods incrementally. A field is first soft-deprecated (the code still works, a warning is logged), then removed in a later version. If you are jumping multiple major versions at once, you may skip the warning entirely and land directly on the removal.
What the symptom looks like. An AttributeError: 'sale.order' object has no attribute 'invoice_ids'-style error, where a field or method your code calls simply does not exist anymore. Or a subtler version: the field exists but returns nothing because it was replaced by a differently-named successor field populated by a different mechanism.
The underlying change. Odoo periodically restructures models to clean up technical debt. Fields get renamed, merged, or moved to a related model. Methods get replaced by a more general version. The old name disappears. The change is usually documented in the Odoo upgrade guide, but the upgrade guide is written for standard Odoo, not for the specific places your custom code reached into those fields.
Real examples from recent versions. Two that come up regularly in our upgrade work: the team_id (Sales Team) field on res.partner was deprecated in v18, and the mobile field on res.partner was deprecated in v19. Both are the kind of field that gets used casually in custom code (reports, computed fields, domain filters) without anyone thinking of it as a dependency risk. When the field disappears, every place your code referenced it breaks, often in ways that are not immediately obvious from the error message alone.
The fix pattern. The reliable approach is a static analysis pass before the upgrade. Build a list of every Odoo field and method your custom code references, then check each one against the target version's source. This is tedious but mechanical. In PyCharm, a project-wide find on a field name across your custom addons tree takes seconds; cross-checking it against the Odoo source in the same project takes a few minutes per field. Do not rely entirely on runtime errors to discover these. Some deprecated field replacements behave correctly at first glance and fail only under specific data conditions.
A note on jumping versions: if your deployment is two or three major versions behind and you are considering jumping directly rather than stepping through each version, the deprecated-fields category is the one most likely to argue for stepping. The cumulative deprecation surface across multiple versions is large, and the interaction effects compound.
4. JavaScript asset bundling changes
Odoo's mechanism for declaring, bundling, and loading JavaScript and CSS assets has changed multiple times across major versions. Custom modules that declare assets in the older format will either fail to load at all or load in the wrong order, producing hard-to-diagnose UI bugs.
What the symptom looks like. A custom module installs cleanly and Python-side works fine, but something on the frontend is broken or missing. The browser console may show a 404 on an asset file, or no error at all (the script simply was not included in the bundle). In more confusing cases, the asset loads in development mode but not in production mode, because the two modes bundle differently.
The underlying change. Early Odoo versions declared JavaScript and CSS assets inside QWeb templates using specific bundle names (web.assets_backend, web.assets_frontend, and so on). The mechanism for adding to those bundles, the XML syntax for doing it, and the set of available bundle names have all changed across versions. A module written for the older declaration pattern may produce a silent no-op in a version that expects the newer pattern, with no install-time error to flag the problem.
The fix pattern. For each custom module that ships JavaScript or CSS, locate every asset declaration (typically inside a ir.asset record or a QWeb template that inherits an asset bundle) and verify the syntax and bundle name against the target version's documentation and existing Odoo modules. The most reliable cross-check is to look at how a standard Odoo module that ships assets declares them in the target version and match that pattern exactly.
Test in production mode, not just debug mode, before signing off. Debug mode bypasses the bundler. Production mode exercises it. A bug that only appears in production mode after an upgrade is embarrassing and avoidable.
5. Security group and record rule changes
Custom security groups and record rules are among the most brittle parts of an Odoo upgrade because they depend on specific XML IDs in Odoo's own security files, and those IDs occasionally change or the groups they reference are restructured.
What the symptom looks like. After upgrade, users lose access to records they previously had access to, or gain access they should not have. Sometimes the symptom is a cryptic access error on a model the user has always been able to read. Sometimes it is more subtle: a field is missing from a view because a field-level access rule no longer evaluates the way it did.
The underlying change. Two things typically go wrong. First, a custom record rule that references a standard Odoo group via XML ID fails because that group was renamed or restructured in the new version. The rule still installs (the XML ID might still resolve), but the semantics changed. Second, Odoo occasionally tightens its own access control in ways that interact with custom rules: a model that was readable by all internal users in one version may require a specific group in the next.
The fix pattern. Before the upgrade, extract a full list of your custom security records: access control lists (ACLs), record rules, and any group XML IDs your rules reference. Cross-check every referenced standard Odoo group XML ID against the target version's security/ files. After the upgrade, UAT (User Acceptance Testing) should be performed with actual user accounts at each role level, not just as the administrator. Administrator bypasses most record rules, which means admin-only testing will miss a majority of security regression bugs.
How we sequence an upgrade
The five categories above are not independent. An ORM change can mask a deprecated-field bug. A security regression can look like a data problem. The order in which you fix things matters.
Our sequencing: get the modules to install cleanly first. Nothing else is diagnosable until the install is clean. Then do a static review pass: deprecated fields, method signatures, asset declarations, security XML IDs. Then bring up a test instance and run through every workflow that touches custom code. Then test in production mode. Then UAT with real user accounts at every access level. Only then do we consider the upgrade ready for production.
We do this iteratively. We do not hand the client a finished upgrade; we walk through a working draft, get feedback on what is broken or missing, fix it, and come back. The surprises that survive a static analysis pass almost always surface during that first working-draft walkthrough, when someone who actually uses the system tries to do their job on it.
If you are behind on Odoo versions and trying to decide whether to step through each major version or jump directly to the current one, that decision depends on your specific codebase: how many custom modules you have, which Odoo models they touch, and how far behind you are. There is no universal right answer. DimeSoft offers version-upgrade assessments that start with a real look at your code before any commitment to a scope or budget. If you are in that planning stage, a conversation is the right next step.