A real wholesale case, anonymized. A 230 GB ERP with 61 custom fields was ready to jump from Odoo 16 to Odoo 19. Before migrating, we cleaned up the truth of its inventory. Along the way we discovered that the costing method was hiding, on at least one product, the fact that the company was selling with no margin at all.
Migrating a large ERP is not copying a system from one version to another. It also carries over everything that system had wrong. When the foundation is dirty, migration does not clean it: it inherits it, makes it more expensive to fix, and on top of that slaps on the reassuring label of "new system." This piece describes how a wholesale distributor of lighting and project furniture in Mexico got ready to upgrade from Odoo 16 to Odoo 19, and why the first deliverable was not the migration, but the stabilization of its inventory.
The result of that preliminary phase was uncomfortable and valuable at the same time: the configured costing method was distorting the financial reading of the business. What follows are the real numbers from the diagnostic, anonymized, and the transferable principle they left behind.
The mirage of the clean migration
The most common advice when an Odoo version nears its end of support is blunt: upgrade. Odoo 16 would stop receiving support, the calendar pressure was real, and the obvious route seemed to be moving the database to version 19 and carrying on operating.
The problem is that "just upgrade" assumes that what gets carried over is healthy. In this case it was not. The starting point was an ERP with history:
- A database of 230 GB, bloated by years of transactions with no period cutoffs.
- 61 custom fields that would each have to be updated one by one so they would not break in the new version.
- Around 29,000 lines of Python code and 12,000 of XML in accumulated customizations.
Migrating all of that without reviewing it first is not a migration; it is moving house without opening the boxes to see what is inside. And inside there was a problem no version upgrade was going to solve on its own.
The diagnostic: an ERP that no longer told the truth
Before touching the version, we did what almost no one does: instead of trusting the numbers the system displayed, we put them to the test. The question was not "how do we migrate," but "can we trust what this inventory is telling us today."
The answer came through costing. A wholesale distributor lives on a margin that is often thin; if the system miscalculates the cost of what it sells, the company makes pricing decisions based on a reality that does not exist. That is why the core of the stabilization was to validate, with real products, the three costing methods Odoo can apply.
The test almost no one runs: validate costing before you trust it
Odoo lets you value inventory using three methods. In two lines each, for anyone who does not live in cost accounting:
- Standard price. A cost per product is set and does not change with each purchase. Simple, but it drifts from reality when supplier prices move.
- FIFO (first in, first out). Whatever comes in first goes out first, and its original cost travels with the sale. Good for lot traceability.
- Weighted average cost. Each purchase recalculates a single average cost. Stable in the face of multiple receipts, ideal for fast rotation.
It is not about which is "the best" in the abstract. It is about which one is correctly applied for each product family, and above all whether the numbers it produces are believable. Three scenarios were documented, one per method, with real products from the catalog. And that is where the finding appeared.
The climax: when costing lies
In the FIFO scenario, on a real product from the acoustic treatment category, the numbers told a story the operation had not noticed:
- Commercial margin of 0%. The system recorded a sale that, according to its own costing, left no profit at all.
- Deflation of -36.6% in the inventory valuation of that product. The book value was eroding silently.
- Rotation of 11.4%, very low: a product that moves little and ties up capital.
- 990 units with no clear origin documentation. Stock the system held, but whose intake no one could trace.
Each of those figures, on its own, is a yellow warning light. Together, on a single product, they are a diagnostic: the inventory was not a source of truth, it was a source of misplaced confidence. Had that foundation been migrated as-is to Odoo 19, the company would have rolled out a more modern system to keep making decisions on distorted data. The new version does not fix badly grounded costing; it inherits it with a better appearance.
The cure was not to re-implement, it was to stabilize
It is tempting to answer a finding like this with a full redesign. It was not necessary. The cure was surgical: give the inventory back its traceability, not reinvent it.
A central piece of that work was the bulk stock adjustment, done the right way. In a wholesale catalog with thousands of references, two products can share a name and still be different: same commercial name, different brand or line. If you try to adjust inventory by name or by internal reference, Odoo does not know which of the two to apply the change to and fails, or worse, applies it to the wrong one.
The solution was to anchor each adjustment to the unique external identifier Odoo assigns to every variant, not to its name. With that anchor, the bulk load stops guessing: each counted quantity lands on the correct variant, with its date and its location. Physical stock and system stock line up again, which is the minimum condition for any subsequent costing to mean anything.
Stabilize before you migrate, the full flow
Order matters. This is the flow that separated stabilization from migration, and why each step comes before the next.
flowchart LR
A[Legacy ERP<br/>230 GB, 61 fields] --> B{Are the numbers<br/>believable?}
B -->|Validate costing<br/>3 methods, real products| C[Finding:<br/>0% margin, -36.6% deflation]
C --> D[Stabilize:<br/>adjust by external ID]
D --> E[Reconciled inventory<br/>physical = system]
E --> F{Now yes:<br/>migrate to v19}
F --> G[Odoo 19<br/>on a trustworthy base]
classDef riesgo fill:#3d0a0a,stroke:#f87171,stroke-width:1px,color:#fee2e2;
classDef prueba fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#e2e8f0;
classDef cura fill:#052e16,stroke:#34d399,stroke-width:1px,color:#e2e8f0;
class A,C riesgo
class B,D prueba
class E,F,G cura
Diagram: in red the inherited risk, in purple the tests that reveal it, in green the trustworthy base on which migrating is finally worth it.
The transferable principle
What this wholesaler learned applies to any distributor running Odoo and thinking about a version upgrade. It is not a product recipe, it is a sequence of judgment:
- Do not confuse upgrading with cleaning up. End of support forces you to migrate; it does not force you to migrate the mess. They are two projects, and the cleanup goes first.
- Put costing to the test with real products, not with theory. A well-chosen method that is badly applied produces numbers that look healthy and are not.
- Treat a 0% margin as an alarm, not as a data point. In wholesale, a margin the system reports at zero is almost never reality; it is a signal that costing lost the thread.
- Adjust inventory by unique identifier, never by name. In large catalogs, names repeat. The external identifier is the only reliable anchor.
- Reconcile physical against system before migrating. If those two numbers do not match today in the old version, they will not match tomorrow in the new one; they will just look more official.
- Migrate when the base tells the truth, not when the calendar squeezes. The urgency is real, but a migration on distorted data costs more to fix afterward than to stabilize beforehand.
Closing: migration is the second problem
The underlying lesson is simple and counterintuitive. When a company feels that "its inventory does not add up" or that "the margin the system reports does not feel real," the instinct is to migrate to something better. But the ERP version is almost never the problem. The problem is that the base lost its truth, and no new software gives it back on its own.
Stabilizing before you migrate is not just one more technical step. It is the difference between rolling out a trustworthy system and rolling out one that lies with a better interface. First the truth of the inventory. Then the version.
If your distribution business runs Odoo and is thinking about a version upgrade, the conversation starts at [email protected]. We bring the stabilization method and the judgment for when it does, or does not, make sense to migrate yet.
Note on accuracy and privacy
The figures in this article (a 230 GB database, 61 custom fields, close to 29,000 lines of Python and 12,000 of XML, and in a FIFO costing scenario: a 0% margin, -36.6% deflation, 11.4% rotation, and 990 units with no documented origin) come from the stabilization documentation of a real wholesale project executed between January and June 2026. No figure was estimated or rounded. The client is presented anonymously: the company, its brands, its suppliers, and the people involved are not named, and the article focuses exclusively on the technical learning. No personal data was read or cited; only operational aggregates.