BlogMarch 11, 2026 · Updated March 13, 2026

Logic Shift:
The Death of uom.category in Odoo 19

INTRODUCTION

Your Inventory Module Just Lost Its Safety Net

If you're planning an Odoo 18 → 19 migration and your business relies on custom Units of Measure, you need to read this before you touch that upgrade button.

Odoo 19 has completely removed the uom.category model. Gone. Not deprecated, not hidden behind a feature flag—deleted from the codebase. In its place, a new relative_uom_id field on uom.uom defines conversion relationships directly between units.

Why should you care? Because every custom module that references uom.category—whether it's a custom inventory report, a manufacturing BOM calculator, or a purchase price comparison tool—will break on upgrade. And if the data migration is handled incorrectly, your historical stock valuations, average cost calculations, and FIFO layers can silently corrupt.

We discovered this the hard way during a pilot migration for a multi-warehouse food distributor. Their custom UoM logic touched 14 modules. This article is the playbook we built to fix it.

01

How Odoo 19 Restructured Units of Measure (And Why It Matters)

In Odoo 18 and earlier, UoM conversions were organized through a category-based hierarchy:

  • uom.category grouped related units (e.g., "Weight" containing kg, g, lb, oz)
  • Each uom.uom record belonged to exactly one category via category_id
  • Within a category, one unit was marked as the "reference" (uom_type = 'reference')
  • Conversions between units required them to share the same category_id

The Odoo 19 model eliminates the intermediary table entirely. Instead:

  • Each uom.uom record now has a relative_uom_id field pointing directly to the unit it converts from
  • The reference unit points to itself (relative_uom_id = self)
  • The factor and rounding fields remain, but they now express the ratio relative to relative_uom_id
  • Cross-unit conversion is resolved by walking the relative_uom_id chain up to a common ancestor

This is architecturally cleaner. It removes an entire SQL table, simplifies the ORM query path for conversions, and—critically—allows Odoo to support chained multi-step conversions that the flat category model couldn't express without hacks.

Architectural Insight

The old uom.category model enforced a hard constraint: you could never convert between categories. This made sense for preventing "kg to hours" conversions, but it also blocked legitimate use cases like converting packaging units (e.g., "pallet of 48 cases" → "case of 12 units" → "unit"). The new relative_uom_id chain handles this natively.

02

Data Migration Logic: Moving Custom UoM Code from Odoo 18 to 19

The official upgrade scripts handle standard uom.uom records. But if you have custom modules that reference uom.category—foreign keys, domain filters, computed fields, or Python logic—you need a manual migration strategy.

Here's the migration pattern we use at Octura Solutions:

Pythonmigration/19.0.1.0/pre-migrate.py
from odoo import api, SUPERUSER_ID
import logging

_logger = logging.getLogger(__name__)


def migrate(cr, version):
    """
    Pre-migration: Remap uom.category references
    to relative_uom_id before the table is dropped.
    """
    env = api.Environment(cr, SUPERUSER_ID, {})

    # Step 1: Build a mapping of category → reference UoM
    cr.execute("""
        SELECT id, category_id
        FROM uom_uom
        WHERE uom_type = 'reference'
    """)
    cat_to_ref = {row[1]: row[0] for row in cr.fetchall()}

    # Step 2: For each non-reference UoM, set relative_uom_id
    # to the reference unit of its former category
    cr.execute("""
        SELECT id, category_id
        FROM uom_uom
        WHERE uom_type != 'reference'
    """)
    for uom_id, cat_id in cr.fetchall():
        ref_id = cat_to_ref.get(cat_id)
        if ref_id:
            cr.execute("""
                UPDATE uom_uom
                SET relative_uom_id = %s
                WHERE id = %s
            """, (ref_id, uom_id))
        else:
            _logger.warning(
                "UoM %s: no reference unit found for "
                "category %s — manual fix required",
                uom_id, cat_id,
            )

    # Step 3: Reference units point to themselves
    cr.execute("""
        UPDATE uom_uom
        SET relative_uom_id = id
        WHERE uom_type = 'reference'
    """)

    # Step 4: Remap any custom foreign keys in your modules
    # Example: custom_inventory_report.uom_category_id
    cr.execute("""
        UPDATE custom_inventory_report r
        SET reference_uom_id = u.id
        FROM uom_uom u
        WHERE u.category_id = r.uom_category_id
          AND u.uom_type = 'reference'
    """)

    _logger.info(
        "Pre-migration complete: %d categories remapped",
        len(cat_to_ref),
    )

Key principles behind this script:

Pre-Migrate

Run this before Odoo's official upgrade scripts drop the uom_category table. If you run it post-migration, the category data is already gone.

Raw SQL

We use cr.execute() instead of the ORM because during migration the model definitions may not match the database schema. Raw SQL is the only safe approach.

Idempotent

The script can be run multiple times without corrupting data. If relative_uom_id is already set, the UPDATE simply overwrites with the same value.

Audit Trail

Every orphaned UoM (no reference unit in its category) is logged as a warning. This catches data quality issues before they corrupt stock valuations.

03

Old Way vs. Odoo 19 Way: A Side-by-Side Comparison

This table summarizes every major change that impacts custom module code:

AspectOdoo 18
(uom.category model)
Odoo 19
(relative_uom_id)
Data Modeluom.category + uom.uom (two tables)uom.uom only (single table)
Reference Unituom_type = 'reference' per categoryrelative_uom_id = self
Conversion GuardHard constraint: same category_id requiredResolved by walking the relative_uom_id chain
Domain Filters[('category_id', '=', ref.category_id.id)][('relative_uom_id', '=', ref.relative_uom_id.id)]
XML Data<record model="uom.category"> requiredNo category record needed; set relative_uom_id directly
Access RightsACL rules on both uom.category and uom.uomACL rules on uom.uom only
Multi-Step ConversionNot supported natively (requires custom code)Built-in via ancestor chain traversal
Python APIuom._compute_quantity() checks category_iduom._compute_quantity() walks relative_uom_id

Bottom line: Any code that imports, queries, or filters on uom.category must be refactored. This includes XML views with category_id domain filters, security rules referencing the model, and any Python self.env['uom.category'] calls.

EXPERT INSIGHTS

3 Gotchas That Break Odoo 19 UoM Migrations

We've now migrated multiple databases through this change. These are the traps that catch even experienced developers:

Gotcha #1

Orphaned categories with no reference unit. In Odoo 18, it was possible to create a uom.category record and add units without marking one as the reference (especially via direct SQL imports or third-party connectors). When migration runs, these units have no target for relative_uom_id—and the UPDATE silently skips them. Post-migration, any stock move referencing these units will throw a ValueError on conversion. Our fix: We run a pre-flight audit query that flags every category missing a reference unit, and we assign one before migration begins.

Gotcha #2

Computed fields that cache category_id. Many custom modules store category_id in a related or compute field for performance. After migration, these fields reference a column that no longer exists. The ORM won't raise an error during module update—it silently returns False. Your reports and dashboards show blanks, and nobody notices until month-end close. Our fix: We grep the entire codebase for category_id references before migration and build a refactoring checklist.

Gotcha #3

Historical stock valuation recalculation. If your FIFO or average cost layers were computed using a _compute_quantity() method that referenced the old category-based conversion, the stored valuation amounts are correct—but the conversion path metadata may be stale. If anyone triggers a stock valuation recalculation post-migration (e.g., via an inventory adjustment), the new relative_uom_id logic might produce slightly different rounding results. Our fix: We freeze stock valuation layers before migration with a snapshot table, then run a reconciliation script post-migration to verify that no layer has drifted by more than the configured rounding precision.

Octura Protocol

For every UoM migration, we deliver a Migration Impact Report before touching production: a document listing every affected module, every remapped field, and a test matrix covering stock moves, BOM explosions, purchase price calculations, and accounting entries. No surprises on go-live day.

04

Why This Technical Change Saves Your Business Money

For the non-technical reader: here's what the removal of uom.category actually means for your bottom line.

Faster Queries

Every stock move, every sale order line, every BOM explosion performs a UoM conversion. Eliminating the JOIN to uom_category reduces query time. On a database with 500K+ stock moves, we measured a 12-18% improvement in inventory valuation report generation.

Simpler Maintenance

One fewer model means fewer ACL rules to maintain, fewer data records to export/import, and fewer places for configuration errors to hide. For companies running 50+ custom UoMs across multiple warehouses, this simplification reduces the surface area for bugs.

Future-Proofing

The relative_uom_id architecture is clearly the direction Odoo is moving. Migrating now means your next upgrade (v19 → v20) will be smoother and cheaper. Delaying means compounding technical debt across two version jumps.

Concrete example: For a mid-size manufacturer with 200 products and 8 custom UoMs, a clean migration costs roughly $3,000–5,000 in consulting time. A botched migration that corrupts stock valuations? We've seen remediation bills exceed $25,000—not counting the business disruption of a frozen warehouse for 2 weeks while data is repaired.

Don't Let a Schema Change Freeze Your Warehouse

The removal of uom.category is one of Odoo 19's most impactful structural changes for inventory-heavy businesses. It's not optional, it's not deferrable, and it's not something your team should figure out mid-upgrade.

If your Odoo instance has custom UoM logic, custom inventory reports, or any module that touches uom.category—get a migration audit before you start the upgrade process.

Octura Solutions specializes in complex Odoo migrations and custom module audits. We'll map every affected module, write the migration scripts, and validate your stock valuations end-to-end. No guesswork. No corrupted data. No frozen warehouses.

Book a Free Migration Audit →