DEV Community

Cover image for Odoo Development Services in Python: How to Write Upgrade-Safe Modules
Sanya Mittal
Sanya Mittal

Posted on Edited on

Odoo Development Services in Python: How to Write Upgrade-Safe Modules

A custom module that overrides sale.order.action_confirm without calling super() works until the next major release. Then confirmation silently skips logic from other modules, or the module refuses to load. This shows up at the first upgrade after go-live, when a customized Odoo instance meets changed core code. This article shows the pattern we use in Odoo development services to keep custom code upgrade-safe: extend instead of override, ship a migration script, and test the flows that earn money. It explains how Odoo development works on real enterprise projects, with Python snippets for Odoo 18 and PostgreSQL.

Context and Setup

Odoo development services Upgrade-safe code loads, migrates its data, and passes its tests on the next Odoo major version without a rewrite. Odoo releases a major version every year, so every override of core behavior becomes a recurring maintenance cost. McKinsey and Oxford University (2012) analyzed more than 5,400 IT projects and found that large ones ran 45 percent over budget on average. Treat upgrade cost as part of build cost.

Prerequisites:

Odoo 18 and a custom addon named dispatch_note
A staging database restored from production
PostgreSQL and shell access on the staging host
The Odoo Development Services Pattern: Extend, Migrate, Test

Step 1: Extend Core Models, Do Not Replace Them

Extend a core model with _inherit and call super() in every method you touch. Inheritance keeps core logic and other modules’ overrides in the call chain.

Declare _inherit without a new _name.
Add fields instead of changing existing ones.
In overridden methods, call super() and add logic around its result.
python
from odoo import fields, models

class SaleOrder(models.Model):
_inherit = "sale.order" # Why: extends the core model without copying its code

x_dispatch_note = fields.Char()  # Why: a new field leaves core columns untouched

def action_confirm(self):
# Why: super() first keeps core logic and other modules' overrides running
res = super().action_confirm()
for order in self:
if not order.x_dispatch_note:
order.x_dispatch_note = order.name # default applied after core confirmation
return res

Enter fullscreen mode Exit fullscreen mode




Step 2: Ship a Migration Script With Every Schema Change

A migration script moves existing data when a module version changes, so new fields are never empty on old records.

Bump version in manifest.py to 18.0.1.1.0.
Add migrations/18.0.1.1.0/post-migration.py.
Write set-based SQL so the script does not depend on ORM changes.
python

dispatch_note/migrations/18.0.1.1.0/post-migration.py

def migrate(cr, version):
# Why: one set-based statement, independent of ORM changes between versions
cr.execute("""
UPDATE sale_order
SET x_dispatch_note = name
WHERE x_dispatch_note IS NULL
""")
Step 3: Test Core Flows and Rehearse the Upgrade

Tag tests to run after all modules install, then rehearse the upgrade on a staging copy before any cutover.

python
from odoo.tests import TransactionCase, tagged

@tagged("post_install", "-at_install") # Why: runs once all modules are loaded, so core flows are complete
class TestDispatchNote(TransactionCase):
def test_confirm_sets_note(self):
partner = self.env["res.partner"].create({"name": "Test Co"})
product = self.env["product.product"].create({"name": "Widget"})
order = self.env["sale.order"].create({
"partner_id": partner.id,
"order_line": [(0, 0, {"product_id": product.id, "product_uom_qty": 1})],
})
order.action_confirm() # Why: exercises core confirmation plus our extension
self.assertEqual(order.x_dispatch_note, order.name)
bash

Why: update only our module on a production-like copy to surface load errors early

odoo-bin -d staging_copy -u dispatch_note --stop-after-init

Why: run only this module's tests, so failures point at our code

odoo-bin -d staging_test -i dispatch_note --test-enable --test-tags /dispatch_note --stop-after-init

The trade-off is first-release speed. An override ships sooner but couples you to core internals. Studio and automated actions need no code, yet they are harder to version-control and test. We choose extension plus tests for anything touching sales, stock, or invoicing, and accept the slower start.

Real-World Application

In one of our Odoo development services projects at Oodles, an anonymized client running Odoo 18 asked us to rehearse a major-version upgrade before committing to a cutover date. The first staging run failed at module load. A custom order module overrode a core confirmation method without calling super(), so stock reservations from a second module never fired. We rewrote the override as an _inherit extension, added a post-migration script to backfill [CONFIRM: field or table], and added post_install tests for confirm, deliver, and invoice.

Key Takeaways

Upgrade cost follows coupling: modules that override core methods create rework, and modules that extend core rarely do.
Calling super() first keeps other modules’ overrides in the call chain.
A versioned post-migration.py with set-based SQL keeps data fixes independent of ORM changes.
Tests tagged post_install check your code against the finished core flow.
Rehearse -u on a production-like copy before every cutover.
Questions or War Stories?

Share them in the comments. To get your module list reviewed, talk to our Odoo development services team.

**What are Odoo development services?
**Odoo development services cover implementation, custom module development, integrations, data migration, upgrades, and support. The technical differentiator is how custom modules are built, because inheritance, migration scripts, and tests decide long-term upgrade cost.

**Why do Odoo custom modules break after an upgrade?
**Odoo development services Most breakage comes from overriding core methods without super(), depending on renamed fields or changed internals, and missing data migrations. Modules that extend core through _inherit and ship migration scripts break far less often.

How do I write an Odoo migration script?
Bump the module version in manifest.py, add migrations//post-migration.py, and define migrate(cr, version). Use set-based SQL for data fixes. Odoo runs the script automatically when the module updates to that version.

Should I use Odoo Studio or custom Python modules?
Odoo development services Use Studio for simple fields and views that business users maintain. Use Python modules for logic touching sales, stock, or accounting, because code can be version-controlled, reviewed, tested, and migrated deliberately.

How do I test an Odoo module before upgrading?
Restore production to a staging database, run odoo-bin -u your_module --stop-after-init, then run tagged tests with --test-enable --test-tags /your_module. Fix load errors and failing flows before scheduling cutover.

Top comments (0)