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'.
Worker — CancellationWorker (poll 10 s) picks it up.
Call — GLINK_CANCEL_ORDER_NOTIFY with
orderNumbers = [PickingNoteNo] (our own order number, not WMS's).
Possible responses
| Response | Action |
|---|---|
SUCCESS | Delete Z_WmsPickingNoteStatus rows, write Z_WmsCancelHistory, release SOs. |
ORDER_NOT_EXIST | Same 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.
Worker — AsnCancellationWorker (poll 10 s).
Call — GLINK_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 state | WMS response | Post state |
|---|---|---|
Received, Allocated, Picking, Picked, Packing, Packed | SUCCESS | Voided |
Shipped | INVALID_OPERATION ("Current order status is not allowed to be canceled.") | unchanged |
Closed | INVALID_OPERATION | unchanged |
| Never existed / already cancelled | ORDER_NOT_EXIST | n/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)
PickingNoteWorkernow stampsWmsStage = COALESCE(WmsStage, 'Received')right afterCreateOrderAsyncsucceeds.CancellationWorkergates onWmsStage IS NOT NULL OR WmsOrderNumber IS NOT NULL.ORDER_NOT_EXISTtreated 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
| Scenario | Status flip | Worker pick-up | WMS response | Local cleanup |
|---|---|---|---|---|
| Manual picking cancel (state = Picking) | DtlStatus='PC' at T0 | ≤10 s | ~300 ms | within same poll |
| Manual ASN cancel (state = Received) | CancelRequested=1 at T0 | ≤10 s | ~300 ms | within same poll |
| Orphan picking cancel | PickingNote delete | Within next poll (≤10 s) | varies | status 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.
:::