Units of measure reference
This page is a lookup for the rules that govern a product's units of measure (UoMs): the flags that mark a row as stock, purchase, or sales; how the conversion factor auto-fills for same-family system units; when the stock UoM becomes locked; every validation error the sync endpoint can return; how display precision behaves; and where the per-product UoM set connects to the global units-of-measure catalog.
For the task of adding, editing, and saving UoM rows, see Configure a product's units of measure. For the actual stock numbers a product carries in its stock UoM, see View a product's stock.
The UoM model at a glance
Every product has a set of UoM rows. Each row pairs the product with one global unit and carries a conversion factor plus three flags. Exactly one row is the stock UoM — the canonical unit inventory is tracked in — and every other row's conversion factor is expressed relative to it.
| Field | Type | Meaning |
|---|---|---|
uom_id | integer | The global unit this row references (see Global catalog). |
conversion_factor | float > 0 | How many stock units equal one of this row's units. Always 1 on the stock row. |
display_precision | integer 0–8 | Decimal places used when showing a quantity in this unit. Defaults to 2. |
is_stock_uom | boolean | Marks the canonical inventory unit. Exactly one row must be true. |
is_purchase_uom | boolean | Default unit when purchasing. At most one row true. |
is_sales_uom | boolean | Default unit when selling. At most one row true. |
The conversion factor is the number of stock units contained in one of that row's units. If a product is stocked in Each and you add a Box row where one box holds 12 units, the Box row's factor is 12. Converting a quantity to stock is quantity × factor; converting between two non-stock rows normalizes through stock as (quantity × fromFactor) ÷ toFactor.
Rows are always returned stock-first, then the purchase row, then the sales row, then the rest.
Purchase and sales flags
is_purchase_uom and is_sales_uom let a product be bought in one unit and sold in another — for example, purchased by the Box but sold by the Each.
| Rule | Behavior |
|---|---|
| Default value | Both flags default to false. A product with only a stock UoM has no dedicated purchase or sales unit. |
| At most one purchase UoM | Marking a second row as purchase is rejected on save (see Validation errors). |
| At most one sales UoM | Marking a second row as sales is rejected on save. |
| Client-side uniqueness | Ticking Purchasing or Sales on one row automatically clears the same flag on every other row, so you normally never reach the back end error. |
| Stock row can also be purchase/sales | The three flags are independent — the stock row may also carry the purchase and/or sales flag. |
The purchase UoM is the default unit surfaced when this product is added to a purchase order or a supplier record; see Manage a product's suppliers.
Conversion-factor auto-fill (canonical factors)
When you pick a UoM whose family (weight or volume) matches the stock UoM's family, and both are built-in system units, the editor auto-computes the conversion factor for you. It does this from a canonical table of each unit's size relative to its family base (kilogram for weight, litre for volume):
| Unit | Family | Canonical factor (relative to family base) |
|---|---|---|
KG | Weight | 1 |
G | Weight | 0.001 |
LB | Weight | 0.453592 |
OZ | Weight | 0.0283495 |
L | Volume | 1 |
ML | Volume | 0.001 |
The factor written into the row is pickedCanonical ÷ stockCanonical. For a product stocked in KG, picking G fills 0.001; picking LB fills 0.453592.
Auto-fill has three deliberate limits:
- Count units are excluded.
EACH,BOX, andPALLETaren't in the canonical table because their conversion is product-specific — a box of screws holds a different count than a box of labels. You must type those factors yourself. - It only fills a still-default row. Auto-fill runs only while the row's factor is still the default
1. Once you have typed a custom value, re-picking the unit won't overwrite your override. - It never touches the stock row. The stock row's factor is locked at
1and is skipped.
Cross-family combinations (for example, a weight stock UoM with a volume alternate) and any non-system custom unit are left at 1 for you to fill in manually.
The stock-UoM-change lock
Once a product has transactional history, its stock unit can no longer be changed — reinterpreting existing movements or BOM lines against a different canonical unit would silently corrupt every recorded quantity.
A product has transactional history when any of the following exist for it:
- an inventory movement,
- a BOM component line (the product is used as a component), or
- a BOM output line (the product is produced by an assembly).
When history exists, the editor locks the stock selector: the Stock radio is disabled and a hint reads "Stock unit cannot be changed — product already has inventory or BOM history." The lock only blocks changing which row is the stock UoM — you can still add or remove alternate purchase/sales rows and edit their conversion factors and display precision.
The has_transactional_history flag is returned on each UoM row from the API, so the editor knows to lock the control before you try to save. If a client bypasses the disabled control and posts a changed stock UoM anyway, the sync endpoint still rejects it with the stock-change error — the frontend lock and the back end guard enforce the same rule.
Validation errors
Saving the UoM set posts the whole set to the sync endpoint, which validates in two stages. All UoM-configuration failures return HTTP 422.
Request-shape validation
Checked first, before any business rules:
| Condition | Message |
|---|---|
| No rows submitted | At least one unit of measure is required. |
A uom_id that's not a real global unit | The selected unit of measure does not exist. |
conversion_factor missing, non-numeric, or ≤ 0 | Conversion factor must be greater than zero. |
display_precision outside 0–8 or non-integer | Standard integer/range validation error. |
| A flag that's not a boolean | Standard boolean validation error. |
Configuration (business-rule) validation
Checked after the shape is valid. Each raises the same 422:
| Condition | Message |
|---|---|
| Empty set | A product must have at least one unit of measure. |
| The same unit appears on two rows | Each unit of measure can only appear once per product. |
Any row's factor is ≤ 0 | Conversion factor must be greater than zero. |
The stock row's factor isn't exactly 1 | The stock unit of measure must have a conversion factor of 1. |
| Zero or multiple rows flagged as stock | Exactly one unit of measure must be marked as the stock unit. |
| More than one purchase row | At most one unit of measure can be marked as the purchase unit. |
| More than one sales row | At most one unit of measure can be marked as the sales unit. |
| Stock UoM changed after history exists | The stock unit of measure cannot be changed after the product has been used in inventory movements or BOMs. |
The editor pre-checks the most common of these (a missing unit on a row, exactly one stock unit, and a positive factor) and blocks the save locally with an inline message, so you usually see a friendly prompt rather than a raw 422.
Display precision
display_precision controls how many decimal places a quantity is shown with in that unit — it's a presentation setting only and doesn't affect stored quantities or conversion math.
| Aspect | Value |
|---|---|
| Default | 2 decimal places when unset. |
| Allowed range | 0–8. |
| Stored type | Integer. |
| Applies to | The quantity display for that row (for example, a PALLET row set to 0 shows whole pallets; a KG row set to 3 shows gram-level detail). |
Precision is set per row, so different units on the same product can display at different precisions.
The global units-of-measure catalog (seam)
A product's UoM rows don't define units themselves — each row's uom_id references an entry in the tenant-wide global units-of-measure catalog. The catalog is a separate surface (a manufacturing/settings screen with its own list and create/edit/delete actions) and is out of scope for this reference; a product's UoM editor only picks from it.
Every tenant is seeded with these built-in system units, which can't be deleted:
| Code | Name | Family |
|---|---|---|
KG | Kilogram | Weight |
G | Gram | Weight |
OZ | Ounce | Weight |
LB | Pound | Weight |
L | Litre | Volume |
ML | Millilitre | Volume |
EACH | Each | Count |
BOX | Box | Count |
PALLET | Pallet | Count |
Only these system units — the weight and volume ones — participate in conversion-factor auto-fill. Custom units you add to the global catalog are always available to pick, but you must type their conversion factors by hand.
Next steps
- Configure a product's units of measure — the step-by-step task for editing and saving a product's UoM set.
- Product fields & validation reference — the rest of a product's fields and their rules.
- Set a product's price and Manage a product's suppliers — where the sales and purchase UoMs are used.
- View a product's stock — how stock is counted and reported in the product's stock UoM.