跳到主要内容

Cancel Flows

Cancelling is where most of the business-rule complexity lives. There are four distinct cancel scenarios across the two outbound/inbound paths, plus two blocking guards.

1. Manual picking-note cancel

Trigger — User ticks a row on the Cancel Picking Job tab. Plugin sets Z_PickingNote.DtlStatus = 'PC'.

WorkerCancellationWorker (poll 10 s) picks it up.

CallGLINK_CANCEL_ORDER_NOTIFY with orderNumbers = [PickingNoteNo] (our own order number, not WMS's).

Possible responses

ResponseAction
SUCCESSDelete Z_WmsPickingNoteStatus rows, write Z_WmsCancelHistory, release SOs.
ORDER_NOT_EXISTSame as SUCCESS — WMS never had it.
INVALID_OPERATION (Shipped/Closed)Leave local alone, log, surface to user.

2. Orphan picking-note cancel

Trigger — User deletes Z_PickingNote directly (without ticking cancel first). Z_WmsPickingNoteStatus has a row but the parent is gone.

Worker — same CancellationWorker, second query path.

Call — same. On success, delete the orphan status row.

3. Manual ASN cancel

Trigger — User ticks rows on the PO → ASN Sync tab, clicks "Cancel Synced ASN". Plugin sets Z_WmsPoSyncStatus.CancelRequested = 1.

WorkerAsnCancellationWorker (poll 10 s).

CallGLINK_CANCEL_ASN_NOTIFY.

Guard — GRN must not exist

-- pre-flight
SELECT 1 FROM GRN
INNER JOIN GRNDTL ON GRN.DocNo = GRNDTL.DocNo
WHERE GRNDTL.FromDocNo = @PONo

If the GRN exists (manual or auto-created from WMS callback), block the cancel — goods already booked, reversing from the plugin would leave the ledger inconsistent. Operator must reverse the GRN first.

4. Orphan ASN cancel

Trigger — User deletes the source PO. Z_WmsPoSyncStatus has a row but parent PO is gone.

Worker — same AsnCancellationWorker, second query path.

Call — same GLINK_CANCEL_ASN_NOTIFY.

WMS business constraints

Verified live against the real gateway (2026-04-23):

Pre-cancel stateWMS responsePost state
Received, Allocated, Picking, Picked, Packing, PackedSUCCESSVoided
ShippedINVALID_OPERATION ("Current order status is not allowed to be canceled.")unchanged
ClosedINVALID_OPERATIONunchanged
Never existed / already cancelledORDER_NOT_EXISTn/a

Source: reports/wms-cancel-order-constraints-2026-04-23.md.

The WmsStage gate (bug fix history)

Old code:

if (!string.IsNullOrEmpty(snap.WmsOrderNumber)) // ← only webhook populates
resp = await _orderService.CancelOrderAsync(...);

Z_WmsPickingNoteStatus.WmsOrderNumber is stamped only by the GLINK_UPDATE_ORDER_STATUS webhook, which arrives seconds after push. Any cancel fired in that window silently skipped WMS — resulting in Z_WmsCancelHistory rows that read "Not pushed to WMS (local-only cancel)" even though the order was live on WMS.

Fix (v1.0)

  1. PickingNoteWorker now stamps WmsStage = COALESCE(WmsStage, 'Received') right after CreateOrderAsync succeeds.
  2. CancellationWorker gates on WmsStage IS NOT NULL OR WmsOrderNumber IS NOT NULL.
  3. ORDER_NOT_EXIST treated as success.

Evidence rows PN-26040162 (SUCCESS), PN-26040176 (INVALID_OPERATION), NONEXISTENT-TEST-999 (ORDER_NOT_EXIST) all documented in reports/wms-cancel-order-constraints-2026-04-23.md §Test-evidence.

Timings from test-cases-verified.md

ScenarioStatus flipWorker pick-upWMS responseLocal cleanup
Manual picking cancel (state = Picking)DtlStatus='PC' at T0≤10 s~300 mswithin same poll
Manual ASN cancel (state = Received)CancelRequested=1 at T0≤10 s~300 mswithin same poll
Orphan picking cancelPickingNote deleteWithin next poll (≤10 s)variesstatus row dropped

Caution

:::caution Do not delete from Z_WmsCancelHistory It is the audit trail. Even if a row shows Success = 0, keep it — the ErrorMessage is often the only explanation a support engineer has for "why didn't this cancel". :::

:::caution Shipped-state cancels are a business problem, not a software one If cancelling shipped orders becomes a real user need, the plugin cannot help — that flow has to go through the warehouse's goods-return process, which is a separate WMS API (GLINK_CREATE_RMA_NOTIFY) we do not currently implement. :::