← All articles

Blog

Odoo Customization Without Breaking Upgrades: 5 Principles

Five engineering principles that let you customize Odoo heavily without dreading the next upgrade — written for teams already on Odoo 16, 17, or 18.

4 min read
  • mid

Most teams who hate Odoo upgrades are not paying for “an upgrade”. They are paying to undo three years of bad customization choices made by people who are no longer at the company. The migration script becomes archaeology.

This is avoidable. If you follow five principles from day one, moving from Odoo 17 to 18 to 19 stays a normal weekend job, not a quarterly crisis. Here they are, with the trade-offs that nobody mentions until it’s too late.

1. Never touch core. Inherit everything.

The single biggest cause of painful upgrades is direct edits to core Odoo files — patching sale.py, editing a view in addons/, changing a method body because “it’s just one line”. Every one of those edits becomes a manual merge during upgrade.

The discipline is: every modification lives in a custom module under your own namespace (we use ts_ as a prefix). Models extend via _inherit. Views extend via XPath. Methods are overridden with super() calls. Reports, security rules, data — all of it sits in your module, never in core.

When this is done right, “what did we customize” is answered by listing your own addons folder. When done wrong, it’s answered by git diff against vanilla Odoo across thousands of files.

2. Use Studio sparingly, and version-control what you do use.

Studio is fine for genuinely small things — adding a field to a form, hiding a column, renaming a label. The problem is when Studio becomes the default tool for non-trivial logic. Computed fields with Python expressions, automated actions with multi-step server actions, conditional visibility tied to five other fields — all of this is technically possible in Studio, and all of it becomes invisible to your developers.

Our rule: anything more complex than a field addition or a label change moves into a proper module. And the Studio-generated changes that do remain get exported as .xml data files and committed to Git. If your Studio customizations only exist inside the database, you are one bad restore away from losing them, and every upgrade requires re-doing them by hand.

3. Pin your dependencies. All of them.

Odoo customization rarely lives alone. There’s a Python library for PDF generation, a community module from OCA, a fork of a fork that someone installed in 2022 because it “just worked”. When upgrade time comes, half of those don’t have an 18-compatible version, and nobody knows which ones are load-bearing.

Keep a requirements.txt for Python, and an explicit list of OCA modules with their exact version tags. Better: vendor them into your own repository so you control the upgrade cadence. When you decide to move from Odoo 17 to 18, you should be able to read one file and know everything that needs to move with you.

4. Write data migrations, not data fixes.

Sooner or later, an upgrade will require transforming data — a field changes type, a model gets split, a selection value renames. Two paths exist. The first is to fix the data manually in production after the upgrade (“run this SQL, then this Python script”). The second is to write a proper migration that lives in your module’s migrations/ folder and runs automatically.

Manual fixes look faster the first time. By the third upgrade, they have produced a swamp of undocumented one-off scripts that no one wants to touch. Migration scripts in migrations/<version>/pre-migration.py and post-migration.py are version-controlled, reviewed, and re-runnable. The first time you write one, it feels like overhead. The third upgrade, it feels like a gift from past-you.

5. Test on a copy. Always. Before you commit to the upgrade window.

Almost nobody does this properly. The standard pattern is: schedule the upgrade for Saturday, run it Saturday morning, discover the broken module at 2 PM, panic. The professional pattern is: clone production three weeks before the upgrade, run the full upgrade against the clone, fix every issue in the calm of a normal workday, then schedule the real upgrade only after the dress rehearsal succeeded.

On Odoo.sh this is built in — staging branches make it trivial. On self-hosted, it takes more effort, but it’s still the difference between a controlled go-live and a chaotic one. A Surabaya manufacturer we worked with had thirty-two custom modules; their first proper dress rehearsal caught nine real bugs that would have blown up the production cutover. Each one took fifteen minutes to fix in advance and would have taken hours under pressure.

What good looks like after two years

When these principles are followed, the picture two years in looks like this. Your custom code lives in one repository, all under your namespace. Upgrades are a documented procedure, not a debate. New developers can read the addons folder and understand what was changed and why. The conversation with your vendor or internal team is “this upgrade will take 8–16 hours of work” — a quote, not a guess.

If your current Odoo setup feels like the opposite of that, the fix is not a one-time cleanup. It’s a habit shift, applied to every new customization request from this point forward. We’d be glad to talk through how your codebase looks today and where the highest-leverage cleanups would be — a free one-hour call, no slide deck, just a look at the code.