Skip to content
Back to blog
3 min read

Why your Odoo module breaks on every upgrade

The cause is almost never Odoo. It is how the module was written. Three patterns that guarantee your development survives the next upgrade.

ArchitectureOdooBest practices

Every time a new Odoo version ships, the same scene repeats: someone upgrades staging, half the custom modules break, and the vendor who wrote them quotes almost the same amount it cost to build them in the first place.

After seven years cleaning up that mess — sometimes our own — we can say it plainly: the problem is almost never Odoo. The module was written betting that the core would never change.

1. Never modify the core

The original sin. Someone needs to change how stock.picking behaves, opens the Odoo file, edits the method, and sleeps well. It works perfectly — until the next version's git pull overwrites the file and the change vanishes without a trace.

Odoo has an inheritance system for exactly this:

from odoo import models, api

class StockPicking(models.Model):
    _inherit = 'stock.picking'

    def action_confirm(self):
        # super() first: standard behavior stays alive
        result = super().action_confirm()
        self._millora_validate_pallets()
        return result

The practical difference: when Odoo 19 changes action_confirm, your code still calls super() and inherits the improvement instead of fighting it.

2. Treat computed fields as contracts, not shortcuts

A compute without correct depends is a time bomb. It works in your test because the record happens to recompute, and fails in production when the ORM decides recomputation is not needed.

available_qty = fields.Float(
    compute='_compute_available_qty',
    store=True,   # if you will filter or group by it, it must be stored
)

@api.depends('move_ids.state', 'move_ids.product_uom_qty')
def _compute_available_qty(self):
    for picking in self:
        picking.available_qty = sum(
            move.product_uom_qty
            for move in picking.move_ids
            if move.state != 'cancel'
        )

The rule we apply without exception: if the field is used in a filter, a group by, or a report, it is stored and has explicit depends. Otherwise it does not pass code review.

3. Views get extended with xpath, never replaced

Replacing an entire view is the graphical version of modifying the core. It works, and it locks you out of every UI improvement that comes later.

<record id="view_picking_form_millora" model="ir.ui.view">
    <field name="inherit_id" ref="stock.view_picking_form"/>
    <field name="arch" type="xml">
        <xpath expr="//field[@name='partner_id']" position="after">
            <field name="pallet_id"/>
        </xpath>
    </field>
</record>

If the xpath stops finding its anchor in a new version, Odoo fails to install with a clear message. That is an advantage: you find out in staging, in two minutes, instead of discovering it three weeks later because a user reported that "the field is gone."

What this means when you hire

When evaluating an Odoo development vendor, ask three things:

  1. Do you ever modify core files? (The correct answer is no.)
  2. How do you test the module before delivery? (There must be a staging environment with the client's data.)
  3. What happens when the next Odoo version ships? (There must be a concrete answer, not "we'll see when it gets here.")

If all three answers are vague, the price you are being quoted is not the real price: it is the first payment of several.


In our catalog every development is tested against the versions we claim to support, Odoo 16 through 19. And if you need something that is not there, you can buy hours without sitting through three discovery calls.