跳到主要内容

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.

WorkerPollReadsPushes
PickingNoteWorker15 sZ_PickingNote (DtlStatus='N')GLINK_CREATE_ORDER_NOTIFY
ItemSyncWorker5 sZ_WmsItemSyncConfig + ItemGLINK_BATCH_OPERATE_ITEM_NOTIFY
CancellationWorker10 sZ_PickingNote (DtlStatus='PC') + orphansGLINK_CANCEL_ORDER_NOTIFY
AsnCreateWorker15 sPO + Z_WmsPoSyncStatusGLINK_CREATE_ASN_NOTIFY
AsnCancellationWorker10 sZ_WmsPoSyncStatus.CancelRequested + orphansGLINK_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_WmsPickingNoteStatus upsert: WmsStage = 'Received' (via COALESCE so webhook updates always win later), WmsOrderNumber = <from WMS response>, LastPushDate = now.
  • Z_WmsSyncLog row, 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 mappingItemUOM rows → WMS packagingList:

ItemUOM conditionWMS slotpcsQty
Rate = 1EA1
UOM = 'CT' and Rate > 1CSRate
elseINPRate

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.

GateZ_WmsSyncSetting.PoAsnSyncStarted = '1' (separate toggle from picking sync, so ASN can be paused independently).

Flow

  1. Find POs in Z_WmsPoSyncStatus with PushStatus = 'N'.
  2. Build GLINK_CREATE_ASN_NOTIFY with asnLineItems[].quantity = PODTL.SmallestQty (EA).
  3. On success, mark PushStatus = 'S', WmsStatus = 'Received'.
  4. Write Z_WmsSyncLog row, 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". :::