DEV Community

Samcorp
Samcorp

Posted on

Writing Custom Modules That Survive Version Upgrades

Writing Custom Modules That Survive Version Upgrades
A custom Odoo module can work perfectly today and still become expensive six months later.

The real test is often not:

"Does this module work?"

It is:

"Will we still understand and maintain this module when the next Odoo version arrives?"

That is where good Odoo custom module development starts to look different from simply making a feature work.

Odoo is designed to be extended through modules, inheritance, views, ORM methods, and frontend components. But every unnecessary dependency on internal implementation details increases the amount of work required during an upgrade.

Here are the practices that make custom modules much easier to carry forward.


1. Never Solve Customization by Editing Odoo Core

The fastest-looking solution is often the most expensive later.

Imagine needing to change sales order confirmation.

Editing the original Odoo method might work immediately.

But now your implementation depends on maintaining a modified copy of standard Odoo code.

When the standard method changes in a future release, you have to manually determine:

What changed in Odoo?
+
What did we change?
+
Which changes should survive?
Enter fullscreen mode Exit fullscreen mode

Instead, extend the existing model.

This inheritance-first approach is also central to upgrade-safe Odoo customization, where extensions are kept separate from core functionality so future version changes are easier to manage.

from odoo import models


class SaleOrder(models.Model):
    _inherit = "sale.order"

    def action_confirm(self):
        result = super().action_confirm()

        self._run_custom_confirmation_logic()

        return result

    def _run_custom_confirmation_logic(self):
        # Custom business logic
        pass
Enter fullscreen mode Exit fullscreen mode

The important part is not the exact example.

It is the structure:

Standard Odoo
      ↓
Inheritance
      ↓
Small custom extension
Enter fullscreen mode Exit fullscreen mode

instead of:

Copied Odoo code
      ↓
Modified copy
      ↓
Future merge problem
Enter fullscreen mode Exit fullscreen mode

Odoo explicitly provides model and view inheritance as mechanisms for extending existing functionality.


2. Keep Overrides Small

Even when inheritance is used correctly, an override can still become difficult to upgrade.

Consider a 250-line override of:

action_confirm()
Enter fullscreen mode Exit fullscreen mode

If Odoo changes that method in the next version, understanding the difference becomes painful.

A better pattern is to keep the override thin:

def action_confirm(self):
    result = super().action_confirm()

    self._create_external_reference()

    return result
Enter fullscreen mode Exit fullscreen mode

Then place the actual custom behavior inside methods your module owns.

def _create_external_reference(self):
    ...
Enter fullscreen mode Exit fullscreen mode

Now an upgrade review asks:

Does action_confirm() still exist and is this extension point still appropriate?

instead of:

Which parts of these 250 lines came from Odoo three versions ago?

That dramatically reduces upgrade review time.


3. Declare Dependencies Explicitly

The module manifest is more important than it looks.

A typical module might contain:

{
    "name": "Custom Sales Workflow",
    "version": "1.0.0",
    "depends": [
        "sale",
        "stock",
    ],
    "data": [
        "security/ir.model.access.csv",
        "views/sale_order_views.xml",
    ],
}
Enter fullscreen mode Exit fullscreen mode

Odoo uses module dependencies to determine which modules must be installed and updated before another module. Its current module documentation specifically describes the manifest as the place where module metadata and dependencies are declared.

Avoid depending on modules simply because:

"They are installed anyway."

If your code imports functionality from another module, make that relationship visible.

Explicit dependencies make future failures easier to diagnose.


4. Avoid Deep, Fragile XPath Selectors

Views are one of the most common places where upgrades expose fragile customization.

This kind of selector should make you nervous:

<xpath expr="//form/sheet/group/group[2]/div[3]" position="inside">
Enter fullscreen mode Exit fullscreen mode

Why?

Because it depends heavily on the exact structure of the parent view.

If Odoo rearranges those containers, the XPath may stop matching.

Whenever possible, target something more meaningful.

<field name="client_order_ref" position="after">
    <field name="integration_reference"/>
</field>
Enter fullscreen mode Exit fullscreen mode

Or use a carefully targeted XPath tied to a stable element.

The goal is to depend on:

Business meaning
Enter fullscreen mode Exit fullscreen mode

rather than:

DOM position
Enter fullscreen mode Exit fullscreen mode

Odoo's view inheritance mechanism is specifically designed to apply extension views over parent views using inherited records and targeted selectors.


5. Don't Copy Entire Standard Views

Another common shortcut is:

  1. Copy the original form view.
  2. Modify it.
  3. Replace the standard version.

That gives you control.

It also gives you responsibility for maintaining everything that Odoo changes in that view.

Suppose the next version adds:

New buttons
New fields
New widgets
Improved visibility conditions
Security changes
Enter fullscreen mode Exit fullscreen mode

Your copied view does not automatically inherit those improvements.

A smaller inherited view usually creates a much cleaner upgrade path.

Think:

Standard view
+ 10 lines of customization
Enter fullscreen mode Exit fullscreen mode

instead of:

600-line copied standard view
+ customization
Enter fullscreen mode Exit fullscreen mode

Smaller customization surfaces generally mean smaller upgrade surfaces.


6. Use the ORM Unless You Have a Strong Reason Not To

Raw SQL sometimes has valid uses.

But it should not be the default for ordinary business logic.

Compare:

self.env.cr.execute(
    """
    UPDATE sale_order
       SET integration_reference = %s
     WHERE id = %s
    """,
    [reference, self.id],
)
Enter fullscreen mode Exit fullscreen mode

with:

self.integration_reference = reference
Enter fullscreen mode Exit fullscreen mode

The ORM communicates intent much more clearly and stays inside Odoo's model layer.

It also avoids bypassing behavior that may exist around fields, models, access control, caching, and related framework functionality.

For upgrade-safe Odoo custom module development, keeping business logic at the framework level wherever practical makes future changes easier to reason about.

Raw SQL should generally be isolated to cases where you actually need it—for example, certain migration or performance-sensitive operations—and then covered carefully by tests.


7. Stop Hardcoding Database IDs

This is fragile:

if user.id == 7:
    ...
Enter fullscreen mode Exit fullscreen mode

So is:

group_id = 42
Enter fullscreen mode Exit fullscreen mode

Those IDs belong to a particular database.

A restored database, fresh environment, test database, or upgraded system may not have the same numbers.

Prefer XML/external IDs when referring to known records:

review_group = self.env.ref(
    "my_module.group_order_reviewer"
)
Enter fullscreen mode Exit fullscreen mode

Now the code references the logical record instead of whatever database ID it happened to receive.

This matters especially when the same module needs to run across:

Developer DB
Testing DB
Staging DB
Production DB
Upgraded DB
Enter fullscreen mode Exit fullscreen mode

8. Keep Business Logic Out of Views and Controllers

A module becomes much easier to maintain when the responsibility of each layer is clear.

A useful structure is:

Model
    → Business logic

Controller
    → HTTP/API boundary

View
    → Presentation

Security
    → Permissions

Data
    → Configuration/reference records
Enter fullscreen mode Exit fullscreen mode

If an important calculation exists only inside a controller, another workflow may not be able to reuse it.

Instead of:

class CustomController(...):

    def submit_order(self):
        # 80 lines of business logic
        ...
Enter fullscreen mode Exit fullscreen mode

prefer:

class SaleOrder(models.Model):

    def run_custom_process(self):
        ...
Enter fullscreen mode Exit fullscreen mode

and:

class CustomController(...):

    def submit_order(self):
        order.run_custom_process()
Enter fullscreen mode Exit fullscreen mode

Now tests, cron jobs, UI actions, imports, and API endpoints can share the same business logic.


9. Treat Frontend Customization as an Upgrade Boundary Too

Backend Python is not the only source of upgrade problems.

Custom JavaScript can become expensive when it relies heavily on internal component structure.

Modern Odoo's frontend uses Owl and framework-level components and services.

Before patching frontend behavior, ask:

Can this be done through an existing extension point?

Can a registry be extended?

Can a component be inherited?

Can the change remain isolated?
Enter fullscreen mode Exit fullscreen mode

The more deeply custom JavaScript reaches into private implementation details, the more likely it is to require attention when the web client evolves.

A useful rule is:

Patch the smallest surface necessary.


10. Give Every Important Customization a Test

An upgrade should not depend entirely on someone clicking through every screen and saying:

"Looks okay."

Odoo provides module testing support based on Python's testing infrastructure, and its documentation recommends defining tests alongside the module that introduces the functionality.

Suppose your module adds a rule:

Orders above a certain amount require approval.

Test the business rule directly.

Conceptually:

def test_large_order_requires_approval(self):
    order = self.env["sale.order"].create({
        # test data
    })

    self.assertTrue(order.requires_approval)
Enter fullscreen mode Exit fullscreen mode

Then test the opposite condition too.

def test_small_order_does_not_require_approval(self):
    ...
Enter fullscreen mode Exit fullscreen mode

Tests turn an Odoo upgrade from:

Install new version
↓
Click around
↓
Hope nothing broke
Enter fullscreen mode Exit fullscreen mode

into:

Install new version
↓
Update custom modules
↓
Run tests
↓
Investigate failures
Enter fullscreen mode Exit fullscreen mode

That is a much stronger upgrade workflow.


11. Test Behavior, Not Odoo's Implementation

Tests themselves can become upgrade liabilities.

A brittle test may assert internal implementation details:

Method X must call method Y exactly twice.
Enter fullscreen mode Exit fullscreen mode

But the actual business requirement may simply be:

Approved order must produce the expected result.
Enter fullscreen mode Exit fullscreen mode

Prefer tests around business outcomes.

For example:

Given:
A confirmed order requiring manager approval

When:
An unauthorized user attempts approval

Then:
Approval is rejected
Enter fullscreen mode Exit fullscreen mode

This test can remain useful even if the internal implementation changes.

That is exactly what you want during a version upgrade.


12. Design Data Changes as Migrations

Code is only half the module.

Production modules also accumulate data.

Imagine version 1 stores:

status = "approved"
Enter fullscreen mode Exit fullscreen mode

but a later design introduces:

approval_state = "manager_approved"
Enter fullscreen mode Exit fullscreen mode

Changing the Python field definition does not automatically answer:

What should happen to thousands of existing database records?

That belongs in an upgrade strategy.

Odoo supports module upgrade scripts through a migrate() function, and its current upgrade utilities are specifically intended to help developers adapt stored data when module structures evolve.

Conceptually:

def migrate(cr, version):
    # Transform existing data required by the new module version.
    ...
Enter fullscreen mode Exit fullscreen mode

Think about migrations whenever you:

Rename fields
Replace models
Change stored values
Move data
Change relationships
Remove old structures
Enter fullscreen mode Exit fullscreen mode

A module is not upgrade-safe if only fresh installations work.


13. Avoid Building One Giant "custom" Module

It starts innocently.

custom_company
Enter fullscreen mode Exit fullscreen mode

Then it contains:

Sales customization
Inventory customization
Accounting customization
CRM customization
Website customization
POS customization
Integration code
Reports
Enter fullscreen mode Exit fullscreen mode

Eventually, every change depends on everything else.

Instead, use boundaries that reflect functionality.

For example:

company_sale_extension
company_inventory_extension
company_pos_extension
company_account_extension
company_external_connector
Enter fullscreen mode Exit fullscreen mode

That does not mean turning every tiny feature into a separate addon.

It means keeping unrelated responsibilities from becoming one upgrade problem.

When Odoo changes POS behavior, you should ideally be able to inspect the POS-related customization without reviewing an unrelated accounting report.


14. Keep the Dependency Graph Small

Imagine:

Module A
  ↓
Module B
  ↓
Module C
  ↓
Module D
  ↓
Module E
Enter fullscreen mode Exit fullscreen mode

Changing Module E may now affect everything above it.

Before introducing a dependency, ask:

Does this module genuinely require the other module?

Reducing unnecessary dependencies makes upgrades easier to isolate and test.


15. Read the Upgrade as a Code Review, Not Just a Deployment

A major version upgrade is a useful opportunity to inspect old assumptions.

For each custom module, review:

Models inherited
Methods overridden
Views inherited
XPath selectors
JavaScript patches
Controllers
Cron jobs
Security rules
External APIs
Dependencies
Deprecated behavior
Migration scripts
Tests
Enter fullscreen mode Exit fullscreen mode

Then compare those extension points against the target Odoo version.

Odoo's current upgrade guidance is explicit: if a database contains custom modules, compatible versions of those modules are needed for the target version.

So the best time to make an upgrade inexpensive is not when the upgrade starts.

It is when the module is originally written.

A major Odoo version upgrade can expose changes in models, fields, constraints, views, and custom code, which is why reviewing extension points before migration is so important.


A Simple Upgrade-Friendly Module Structure

A clean addon might look like:

custom_sale_approval/
│
├── __init__.py
├── __manifest__.py
│
├── models/
│   ├── __init__.py
│   └── sale_order.py
│
├── security/
│   ├── security.xml
│   └── ir.model.access.csv
│
├── views/
│   └── sale_order_views.xml
│
├── data/
│   └── approval_data.xml
│
└── tests/
    ├── __init__.py
    └── test_sale_approval.py
Enter fullscreen mode Exit fullscreen mode

The exact structure can grow with the feature.

The important property is that another developer can quickly answer:

Where is the model logic?
Where are the views?
Where is security defined?
Where are the tests?
What does this module depend on?
Enter fullscreen mode Exit fullscreen mode

Odoo's own coding guidelines emphasize consistent module structure and maintainable code because these practices make development, debugging, and maintenance easier.


The Upgrade-Survival Checklist

Before calling an Odoo custom module development task finished, ask:

[ ] Did we modify any Odoo core files?

[ ] Are overrides small?

[ ] Are we calling super() where appropriate?

[ ] Are dependencies explicit?

[ ] Are inherited views minimal?

[ ] Are XPath selectors reasonably stable?

[ ] Did we avoid hardcoded database IDs?

[ ] Is business logic located in reusable model methods?

[ ] Are custom frontend patches isolated?

[ ] Are important workflows tested?

[ ] Do tests verify business outcomes?

[ ] Will existing production data survive future schema changes?

[ ] Is the module responsible for one coherent area?

[ ] Could another developer understand why this customization exists?
Enter fullscreen mode Exit fullscreen mode

If several answers are uncomfortable, the upgrade will probably be uncomfortable too.


Final Takeaway

Upgrade-safe Odoo custom module development is not about predicting exactly what Odoo will change in its next release.

That is impossible.

It is about reducing how much of your code depends on implementation details that you do not control.

The pattern is fairly consistent:

Extend instead of copy

Use stable framework mechanisms

Keep overrides small

Keep dependencies explicit

Use the ORM appropriately

Isolate frontend patches

Write tests

Plan data migrations

Keep modules focused
Enter fullscreen mode Exit fullscreen mode

A well-designed custom module may still require changes during an Odoo upgrade.

That is normal.

The difference is that instead of spending days trying to understand what the module was doing, you can identify the affected extension points, update them, run the tests, migrate the data, and move forward.

The best custom module is not simply one that works on the version you are running today.

It is one that leaves the next developer a reasonable path to the version you will be running tomorrow.


Top comments (0)