UOM Round-Trip
The single most important design decision in the codebase: push EA
to WMS, divide EA by Rate on the way back. This page is a
developer-level reference. For the end-user impact, see
FAQs.
The core rule
PUSH: items[].quantity = PickingNoteDetail.SmallestQty (EA)
CALLBACK: shippedQtyInUom = payload.shippedQuantity / Rate (UOM)
STORE: ShippedQty = shippedQtyInUom (Z_WmsPickingNoteItemStatus)
ShippedQtyEA = payload.shippedQuantity (raw EA, for audit)
Rate is Z_PickingNoteDetail.Rate — AutoCount's
EA-per-UOM convention. Example: a line of 2 CT with Rate = 8
means 1 CT = 8 EA, SmallestQty = 16.
Why it has to work this way
AutoCount is multi-UOM. Pick-lines can carry CT, UN, BX, CTN,
PK, OUTER, whatever the user wants. WMS is strictly EA-only —
packagingList has only EA / INP / CS, and outbound order
items[].quantity is always EA, no unit field.
So somebody has to translate. The choice is: flatten to EA at the boundary, or force the warehouse to understand our UOMs. WMS wins that argument, so we flatten.
Worked examples
Assume SO line = 2 CT, Rate = 8, SmallestQty = 16:
| Scenario | WMS ships | Callback EA | ShippedQty (UOM) | ShippedQtyEA | Transfer SO→IV plugin's PickedQty |
|---|---|---|---|---|---|
| Full | 16 EA | 16 | 2 (CT) | 16 | 2 CT |
| Half | 8 EA | 8 | 1 (CT) | 8 | 1 CT |
| Uneven | 10 EA | 10 | 1.25 (CT, fractional) | 10 | 1.25 CT |
| Nothing | 0 EA | 0 | 0 | 0 | 0 |
Fractional UOMs are a feature, not a bug — they tell the operator that some loose EAs were shipped without completing a carton.
Inbound (PO → ASN → GRN) round-trip
Same pattern, opposite direction:
PO line : 80 CT, Rate=24, SmallestQty=1920
ASN push : asnLineItems[].quantity = 1920 ← EA
WMS receives : 1920 EA
Callback : items[].quantity = 1920 ← EA
GRN creation : 1920 / 24 = 80 ← back to CT
GRN.Qty = 80
GRN.UOM = PODTL.UOM = 'CT'
GRN.UnitPrice = PODTL.UnitPrice (already in UOM terms)
Full design: reports/wms-po-asn-grn-uom-2026-04-23.md.
Where the conversion lives
Outbound push (EA multiplication)
WmsSyncHubApi/Workers/PickingNoteWorker.cs → uses
Z_PickingNoteDetail.SmallestQty directly (already calculated by
AutoCount = Qty × Rate). No math in our code.
Outbound callback (EA → UOM division)
WmsSyncHubApi/Controllers/WmsCallbackController.cs handles
GLINK_UPDATE_ORDER_STATUS. For each item in the payload:
SELECT TOP 1 pnd.UOM, pnd.Barcode, pnd.Rate
FROM Z_PickingNote pn
INNER JOIN Z_PickingNoteDetail pnd ON pn.DtlKey = pnd.DtlKey
WHERE pn.PickingNoteNo = @pn AND pnd.ItemCode = @sku
Then:
decimal shippedInUom = rate == 0
? shippedEA // defensive: don't zero silently
: shippedEA / rate;
Inbound push (EA multiplication)
WmsSyncHubApi/Workers/AsnCreateWorker.cs →
asnLineItems[].quantity = PODTL.SmallestQty.
Inbound callback (EA → UOM division)
WmsSyncHubApi/Controllers/WmsCallbackController.cs handles
GLINK_UPDATE_ASN_STATUS, same division by PODTL.Rate.
Multi-UOM item master mapping
Separate concern from the order round-trip, but same theme — collapse
AutoCount's N UOMs into WMS's 3 slots. See
Workers → ItemSyncWorker and
reports/wms-item-sync-uom-mapping-2026-04-23.md.
Defensive checks
| Situation | Fallback |
|---|---|
Rate = 0 (should never happen) | Store raw EA as UOM qty, don't divide |
Item has no PickingNoteDetail match in callback | Log warning, skip line |
| Fractional result | Store the fraction (e.g. 1.25) — it is a signal, not an error |
Caution
:::caution Do not change SmallestQty semantics
AutoCount's SmallestQty is always in base-EA terms, regardless of
what the user typed in the UI. Our push code trusts this column.
Breaking that trust (by e.g. using Qty × Rate manually) will
silently double-multiply if SmallestQty also gets re-derived
elsewhere.
:::
:::caution Rate can be decimal
Z_PickingNoteDetail.Rate is DECIMAL(18,8). A decimal-rate item
(e.g. 1.5 EA per UOM) is rare but legal in AutoCount. Division stays
as decimal — never cast to int — to preserve the fraction.
:::