跳到主要内容

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:

ScenarioWMS shipsCallback EAShippedQty (UOM)ShippedQtyEATransfer SO→IV plugin's PickedQty
Full16 EA162 (CT)162 CT
Half8 EA81 (CT)81 CT
Uneven10 EA101.25 (CT, fractional)101.25 CT
Nothing0 EA0000

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.csasnLineItems[].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

SituationFallback
Rate = 0 (should never happen)Store raw EA as UOM qty, don't divide
Item has no PickingNoteDetail match in callbackLog warning, skip line
Fractional resultStore 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. :::