Background Workers
All WMS traffic is driven by 5 polling workers hosted inside
WmsSyncHubApi.exe. Each worker runs on its own Task loop with a
fixed poll interval; none of them talk to each other directly — they
communicate through the DB.
| Worker | Poll | Reads | Pushes |
|---|---|---|---|
PickingNoteWorker | 15 s | Z_PickingNote (DtlStatus='N') | GLINK_CREATE_ORDER_NOTIFY |
ItemSyncWorker | 5 s | Z_WmsItemSyncConfig + Item | GLINK_BATCH_OPERATE_ITEM_NOTIFY |
CancellationWorker | 10 s | Z_PickingNote (DtlStatus='PC') + orphans | GLINK_CANCEL_ORDER_NOTIFY |
AsnCreateWorker | 15 s | PO + Z_WmsPoSyncStatus | GLINK_CREATE_ASN_NOTIFY |
AsnCancellationWorker | 10 s | Z_WmsPoSyncStatus.CancelRequested + orphans | GLINK_CANCEL_ASN_NOTIFY |
PickingNoteWorker
Purpose — Push new / modified picking notes to WMS as outbound orders.
Poll — 15 s.
Query (paraphrased)
SELECT pn.PickingNoteNo, pn.LastModified, pns.WmsStage
FROM Z_PickingNote pn
LEFT JOIN Z_WmsPickingNoteStatus pns ON pn.PickingNoteNo = pns.PickingNoteNo
WHERE pn.DtlStatus = 'N' -- new
OR (pn.DtlStatus = 'P'
AND pn.LastModified > pns.LastPushDate) -- modified
Writes on success
Z_PickingNote.DtlStatus = 'P'Z_WmsPickingNoteStatusupsert:WmsStage = 'Received'(viaCOALESCEso webhook updates always win later),WmsOrderNumber = <from WMS response>,LastPushDate = now.Z_WmsSyncLogrow,SyncType = 'ORDER_CREATE'.
Bug fix — WmsStage stamp on push
Previously, WmsStage was only populated by the
GLINK_UPDATE_ORDER_STATUS webhook. If the webhook was delayed (or
misconfigured), a user who hit "Cancel" between push and first webhook
would see the cancel skipped with "Not pushed to WMS (local-only cancel)" — even though WMS had the order.
Fix: stamp WmsStage = 'Received' immediately after CreateOrderAsync
returns success, using COALESCE(WmsStage, 'Received') so any later
webhook value takes precedence.
ItemSyncWorker
Purpose — Push item master changes to WMS.
Poll — 5 s.
Query
SELECT isc.ItemCode, i.LastModified, isc.LastSyncDate, isc.SyncStatus
FROM Z_WmsItemSyncConfig isc
INNER JOIN Item i ON isc.ItemCode = i.ItemCode
WHERE isc.SyncEnabled = 1
AND (isc.SyncStatus = 'N' OR i.LastModified > isc.LastSyncDate)
UOM mapping — ItemUOM rows → WMS packagingList:
| ItemUOM condition | WMS slot | pcsQty |
|---|---|---|
Rate = 1 | EA | 1 |
UOM = 'CT' and Rate > 1 | CS | Rate |
| else | INP | Rate |
Duplicates collapsed by Rate ASC. Full design:
reports/wms-item-sync-uom-mapping-2026-04-23.md.
Auto-sync new fix — the auto-insert path now stamps
SyncEnabled = 1 (was hardcoded to 0), matching the checkbox
semantics.
CancellationWorker
Purpose — Cancel picking notes on WMS.
Poll — 10 s.
Two trigger paths:
1. Manual cancel (DtlStatus='PC')
User ticks the Cancel Picking Job grid → plugin sets
Z_PickingNote.DtlStatus = 'PC'. Worker picks it up, fires
GLINK_CANCEL_ORDER_NOTIFY.
2. Orphan detection
Z_WmsPickingNoteStatus has a row, but the parent Z_PickingNote is
gone (user deleted the picking note without cancelling on WMS first).
Worker fires cancel and cleans up the orphan status row.
Gate condition (post-fix)
WHERE (pns.WmsStage IS NOT NULL OR pns.WmsOrderNumber IS NOT NULL)
AND COALESCE(pns.WmsStage, '') NOT IN ('Cancelled','Voided')
Response handling
SUCCESS/ORDER_NOT_EXIST→ local cleanup, write history.INVALID_OPERATION→ leave alone, surface error to user.
Full spec: reports/wms-cancel-order-constraints-2026-04-23.md.
AsnCreateWorker
Purpose — Push POs to WMS as ASNs.
Poll — 15 s.
Gate — Z_WmsSyncSetting.PoAsnSyncStarted = '1' (separate toggle
from picking sync, so ASN can be paused independently).
Flow
- Find POs in
Z_WmsPoSyncStatuswithPushStatus = 'N'. - Build
GLINK_CREATE_ASN_NOTIFYwithasnLineItems[].quantity = PODTL.SmallestQty(EA). - On success, mark
PushStatus = 'S',WmsStatus = 'Received'. - Write
Z_WmsSyncLogrow,SyncType = 'ASN_CREATE'.
AsnCancellationWorker
Purpose — Cancel ASNs on WMS.
Poll — 10 s.
Manual cancel (CancelRequested = 1)
User ticks the PO → ASN Sync grid → plugin sets
Z_WmsPoSyncStatus.CancelRequested = 1. Worker picks it up.
Orphan detection
Z_WmsPoSyncStatus has a row but the parent PO is gone. Worker
fires cancel, cleans up.
GRN guard
Before firing cancel, worker checks whether a GRN already exists for
the PO. If yes, cancel is blocked — goods are already booked into the
ledger. Full rationale:
reports/wms-cancel-order-constraints-2026-04-23.md §GRN-guard.
Common patterns
The PoAsnSyncStarted vs PickingSyncStarted gates
Both workers pairs honour separate "started" flags so QA can isolate one flow at a time. Default: both off. User toggles via the UI tabs.
Worker base class
Each worker extends BackgroundWorkerBase (name may vary), which
provides:
Task ExecuteAsync(CancellationToken ct)— the poll loop.- Auto-retry with exponential backoff on DB timeout.
- Graceful shutdown on Ctrl-C.
Logging
All workers log to Serilog at Information (one line per poll, only
on work done — not on empty polls) and to Z_WmsErrorLog via
ErrorLogger.Error(...) on failure. See Logging.
Caution
:::caution Do not run two copies of the API Each API instance spawns all 5 workers. Two instances = duplicate pushes. Keep a single running instance per account book. :::
:::caution SnapshotPickingNote was an orphan-detection bug
Earlier versions of CancellationWorker used a snapshot-then-delete
pattern that raced with the picking plugin's own inserts. Fixed in
v1.0; if you extend orphan detection, avoid any pattern that relies
on "the picking-note row existed 10 seconds ago".
:::