Skip to main content

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.

FieldTypeMeaning
uom_idintegerThe global unit this row references (see Global catalog).
conversion_factorfloat > 0How many stock units equal one of this row's units. Always 1 on the stock row.
display_precisioninteger 0–8Decimal places used when showing a quantity in this unit. Defaults to 2.
is_stock_uombooleanMarks the canonical inventory unit. Exactly one row must be true.
is_purchase_uombooleanDefault unit when purchasing. At most one row true.
is_sales_uombooleanDefault 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.

RuleBehavior
Default valueBoth flags default to false. A product with only a stock UoM has no dedicated purchase or sales unit.
At most one purchase UoMMarking a second row as purchase is rejected on save (see Validation errors).
At most one sales UoMMarking a second row as sales is rejected on save.
Client-side uniquenessTicking 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/salesThe 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):

UnitFamilyCanonical factor (relative to family base)
KGWeight1
GWeight0.001
LBWeight0.453592
OZWeight0.0283495
LVolume1
MLVolume0.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, and PALLET aren'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 1 and 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:

ConditionMessage
No rows submittedAt least one unit of measure is required.
A uom_id that's not a real global unitThe selected unit of measure does not exist.
conversion_factor missing, non-numeric, or ≤ 0Conversion factor must be greater than zero.
display_precision outside 0–8 or non-integerStandard integer/range validation error.
A flag that's not a booleanStandard boolean validation error.

Configuration (business-rule) validation

Checked after the shape is valid. Each raises the same 422:

ConditionMessage
Empty setA product must have at least one unit of measure.
The same unit appears on two rowsEach unit of measure can only appear once per product.
Any row's factor is ≤ 0Conversion factor must be greater than zero.
The stock row's factor isn't exactly 1The stock unit of measure must have a conversion factor of 1.
Zero or multiple rows flagged as stockExactly one unit of measure must be marked as the stock unit.
More than one purchase rowAt most one unit of measure can be marked as the purchase unit.
More than one sales rowAt most one unit of measure can be marked as the sales unit.
Stock UoM changed after history existsThe 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.

AspectValue
Default2 decimal places when unset.
Allowed range08.
Stored typeInteger.
Applies toThe 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:

CodeNameFamily
KGKilogramWeight
GGramWeight
OZOunceWeight
LBPoundWeight
LLitreVolume
MLMillilitreVolume
EACHEachCount
BOXBoxCount
PALLETPalletCount

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

Last verified: