Three versions forward, on a live business, without losing a record
One of the largest custom Odoo systems I have worked on, brought up to date without a shutdown — including the parts the vendor rewrote twice underneath us.
- Sector
- Retail group, dozens of branches
- Year
- 2025 — 2026
- Built with
- Odoo · Python · PostgreSQL · Git
The result
- Three major versions forward, on a system the business uses every day
- Zero records lost
- Hundreds of custom features carried across, not rebuilt from scratch
- Tills kept working through two rewrites of the software underneath them
The situation
The business was three major versions behind. That is a familiar place to be: every year the upgrade looks bigger than it did the year before, so it gets postponed again, and the gap grows. Meanwhile the version you are on stops receiving fixes, new hires have never seen it, and the features you are paying for in the newer release stay out of reach.
This system had hundreds of custom features on it, built up over years. That is exactly what makes an upgrade frightening — the customizations are where the business logic lives, and nobody wants to find out the hard way which ones quietly broke.
What I found
The first job was not technical. It was working out what still mattered. A meaningful share of the customizations were no longer used, had been superseded by features that shipped in the standard product, or duplicated something a free maintained module already did better.
Every one of those we could delete was an upgrade we did not have to perform. That audit was the single biggest saving on the project.
What I changed
For what remained, the rule was: understand the change, then apply it — never guess.
- Check the standard product first. The cheapest migration of a custom feature is discovering it became standard.
- A missing file does not mean a deleted feature. Code moves far more often than it disappears. Tracing where it went prevented several unnecessary rewrites.
- Verify behavior, not names. Something can keep its name and change what it does. That is where upgrades go quietly wrong, and it is only caught by testing the outcome.
- Never rename anything without the script that moves the data with it. Renaming without one is silent data loss that shows up weeks later.
Then it was rehearsed on a copy of production, repeatedly, until the run was boring.
The result
Three major versions forward with no data loss and no shutdown. The hardest part was the point-of-sale system, which the vendor rebuilt twice across those versions — the tills kept working, which for a retail business is the only measure that counts.
The system is now on a current version, receiving fixes, and close enough to the standard product that the next upgrade is an ordinary piece of maintenance rather than a project.
For the technically minded
Odoo 16 → 19 across 272 custom modules, roughly 147,000 lines of Python and 45,000 lines
of front-end code. The migration crosses the attrs removal, the tree → list rename,
the ORM’s search/read consolidation, and two full rewrites of the POS front end — the
Odoo 17 move to ES modules and patch(), then the Odoo 18 replacement of the client-side
data layer. Every model or field rename shipped with its openupgradelib migration
script in the same commit. The rules I kept repeating in review are now published as
open-source tooling so they apply whether I am reviewing or not.
Client names and identifying details are withheld deliberately. Every figure on this page comes from a system I can open.