Skip to main content

SOP: Starting & Counting a Receiving Session

Document ID: WMS-REC-001 Version: 1.0 Effective date: 04/30/2026 Owner: Warehouse Operations Manager Next review: [six months from effective date] Applies to: All authenticated WMS users receiving inbound goods against a PO


1. Purpose

This procedure governs how warehouse staff open a receiving session for a purchase order, count goods on the floor — by barcode scan or manual entry — handle the session lock when more than one person tries to count the same PO, and submit the count to a manager for approval.

The receiving session is the system of record for what physically arrived. Every count, every scan (successful or failed), every lock take-over, and every submit is written to audit_logs (entityType = "ReceivingSession") and is permanent. Approval (covered in WMS-REC-003) is what creates InventoryUnit rows; this SOP stops at submit.

2. Scope

In scope:

  • Selecting a PO from the Inventory Planner-backed list at /receiving/purchase-orders
  • Starting a new session and resuming an existing one
  • The lock / heartbeat / release-lock mechanism that prevents two staff from counting the same PO concurrently
  • Counting via the three available methods: scan, +/− quick increment, and Set exact-quantity
  • Submitting the session for manager approval

Out of scope:

  • Filing receiving exceptions (damaged, wrong item, missing, overage, quality issue) — see WMS-REC-002
  • Manager approval / rejection / reopen — see WMS-REC-003
  • Putaway from an approved session (the linked WorkTask of type PUTAWAY) — see WMS-REC-004
  • Generating receiving labels on the ZQ511 (pre-printing or print-on-receive) — see WMS-REC-005
  • Inventory Planner administration — see WMS-INV-004 §5

3. Roles & permissions

Honesty about enforcement. The endpoints used in this SOP (POST /receiving/start, /scan, /batch, /add, /set, /heartbeat, /release-lock, /submit) check authentication only. Any authenticated user can start a session, count, and submit. Role enforcement only kicks in on approve and reject (see WMS-REC-003) and on assigning a specific manager as the approver at submit time (the API rejects assignees that are not ADMIN or MANAGER).

RoleStart a sessionCountSubmitApprove / reject
READONLY
STAFF
MANAGER✓ (WMS-REC-003)
ADMIN✓ (WMS-REC-003)
SUPER_ADMIN✓ (WMS-REC-003)

Operational expectation: the staff member who physically counts a PO submits it for approval but does not approve their own count. The assignee at submit must be a manager other than the counter. The API does not enforce "different person" — manager review at approval time is the control.

4. Procedures

4.1 Selecting a PO and starting the session

Use when: A truck has arrived. You have the PO reference, you've physically verified the pallet/case count looks roughly right, and you're ready to count line by line.

Prerequisites:

  • The PO exists in Inventory Planner. (If it doesn't, escalate per §8 — do not invent a session for a PO that isn't in IP, you'll lose vendor traceability.)
  • A receiving location is configured. The system uses any Location of type RECEIVING first, falling back to type STORAGE. If neither exists, the API returns "No receiving location configured" and the session cannot start.

Steps:

  1. Open the Receiving / Purchase Orders page (/receiving/purchase-orders). The list is fetched from Inventory Planner with a 60-second cache — if a PO you expect is missing, give it up to a minute, then refresh.
  2. Find the PO by reference. The card shows vendor, expected date, and item count. The button on the right reads:
    • Start Receiving → if no session exists
    • Continue Receiving → if a session exists with status IN_PROGRESS
    • Review Approval → if a session exists with status SUBMITTED
  3. Click the button.
  4. Start screen (only on first time): the page shows the PO summary and a receiving-location dropdown. Choose the location where you'll physically stage the count. Click Start.
  5. The system creates the session and routes you to /receiving/session/:sessionId.

Result:

  • A ReceivingSession row is created with status: "IN_PROGRESS", version: 1, lockedBy: <you>, lockedAt: <now>, countedBy: <you>.
  • One ReceivingLine per expected item is created with quantityExpected from the PO and quantityCounted: 0.
  • For variants on the PO that have neither a upc nor a barcode, the system auto-generates a barcode and stores it on the variant — these can then be printed via WMS-REC-005 to label the goods at receipt.
  • The session writes a row to audit_logs with action: "SESSION_STARTED".

⚠ If the PO already has an active session, clicking Start will not create a duplicate. The system either returns you the existing session (if the lock is yours or expired) or returns it in read-only-with-lock-banner mode (if someone else holds the lock). See §4.4.

4.2 Counting via barcode scan

Use when: You're at the goods, the Zebra TC22 is paired, and you can scan the printed UPC, vendor barcode, SKU label, or auto-generated WMS barcode.

Steps:

  1. From the session page, ensure the focused field is the Scan barcode input at the top (the page auto-focuses it; if you've tapped elsewhere, tap the input once to refocus).
  2. Trigger the scan. The TC22 sends the barcode characters followed by Enter, which submits the form.
  3. The barcode is matched in this priority order: line sku, line generatedBarcode, variant upc, variant barcode, variant sku.
  4. On success, the matched line increments by 1 and the scan feedback flashes ✓ {sku}. Phone vibrates briefly. The line's lastScannedAt and scanCount update.
  5. Continue scanning. Each scan is +1 to the matched line.

Failure modes (the scan input does not throw HTTP errors — it returns a result object):

result.errorWhenWhat you see / what to do
NOT_ON_POBarcode matched a known variant but the variant isn't on this POFeedback: "{sku} is not on this PO". Do not try to add the item via §4.3 — file an exception via WMS-REC-002 (WRONG_ITEM).
UNKNOWN_BARCODEBarcode matched no variant in the systemFeedback: "Unknown barcode: {barcode}". The barcode may be the case label (not unit), the vendor's internal code, or a damaged scan. Try the unit barcode; if still unknown, count manually via §4.3 and flag in #warehouse-ops.
SESSION_LOCKEDLock is held by another user, or session status changed mid-countFeedback: "Session is locked by another user" or "Session is {STATUS}". See §4.4. Stop counting.

Every successful and every failed scan writes to audit_logs with action: "SCAN_SUCCESS" or "SCAN_FAILED", including the barcode, the lineId, the scanId, and (on failure) the failure reason.

⚠ Scan once, listen for the beep. The TC22 occasionally double-fires on a hard trigger pull. Watch the line counter — if it incremented by 2 from one scan, decrement via §4.3 (Set to the correct number) immediately. The audit log will keep both scan rows but the line's quantityCounted will be correct.

4.3 Manual counting (Add / Set)

Use when: You can't scan — e.g., barcode is damaged, you're correcting a double-scan, or the goods are pre-counted in cases and you want to enter quantityPerCase × cases directly.

Steps:

  1. From the session page, expand the item list and tap the line you want to update. (Tap is disabled if the session is read-only or locked by another user — see §4.4.)
  2. The detail panel shows the line with + and buttons and a Set Exact option:
    • + / — single-unit add/subtract. Backed by POST /receiving/:sessionId/add.
    • Set Exact — opens the modal, enter the new total. Backed by POST /receiving/:sessionId/set.
  3. Updates are debounced and sent in batches via POST /receiving/:sessionId/batch with an expectedVersion. If two devices changed the same session simultaneously, the second one to flush gets HTTP 409 Version conflict.

Result:

  • The line's quantityCounted updates. The session's version increments by 1 on each successful batch flush.
  • An audit_logs row is written with action: "QUANTITY_UPDATED" (or "QUANTITY_SET" for explicit sets), the lineId, the previous and new quantities.

Common errors:

HTTPAPI messageWhat it means
400"lineId and quantity are required"The request was malformed (UI bug — refresh and retry; if it persists, escalate).
400"Line not found"Line was deleted between page load and the update. Refresh the session.
400"Cannot update: session is {status}"Session was submitted/approved/rejected while you had it open. Refresh; you'll land on the read-only view.
409"Version conflict"Someone else's batch flushed first. The UI auto-refreshes the session and replays your local pending changes — verify the line counts after the auto-refresh before continuing.
400"Session is locked by another user"See §4.4.

⚠ Manual counting is auditable but unverified. Scans are the gold standard — they prove a barcode passed under the reader. A manual entry only proves you typed a number. Use scan whenever possible, especially for cannabis/vape lots and high-value items.

4.4 Resuming a session and the locking mechanism

Use when: You're returning to a session you started earlier; the previous counter went on break; or you got the dreaded "Session is locked by another user" banner.

How the lock works:

  • When you start or resume a session, the system sets lockedBy to your user ID and lockedAt to now.
  • While the session page is open, the UI calls POST /receiving/:sessionId/heartbeat every 60 seconds to refresh lockedAt.
  • The lock is considered valid for 5 minutes after the most recent heartbeat. After 5 minutes of silence, the lock is treated as stale and any other user can take it over by opening the session.
  • The lock is explicitly released when:
    • You leave the session page (the UI calls POST /receiving/:sessionId/release-lock).
    • You submit the session for approval (lock is cleared atomically with the status change).

The lock UI states:

StateWhat you seeWhat you can do
You hold the lockNormal page; Submit button enabledCount, scan, exception, submit.
Lock is yours, but session was submitted/approved/rejected from another tabPage renders read-only with status bannerView only. To re-open a rejected session, see WMS-REC-003 §4.4.
Lock held by another active userRed banner: Locked by {name} at the top; counting is disabled; submit hiddenView the session. Don't count. Wait, or call/Slack the named user to release.
Lock held by another user but stale (>5 min since their last heartbeat)The system silently transfers the lock to you when you open the pageCount normally. The previous holder, if they return, will see the locked-by-you banner.

Steps if you see "Locked by {name}":

  1. Don't refresh repeatedly — the heartbeat is on a 60-second interval, refreshing won't free a lock held by an active user.
  2. Identify whether the named user is currently working that PO:
    • They are → coordinate verbally. One of you counts; the other waits or works a different PO.
    • They walked away without closing the page → ask them to navigate away (auto-releases) or close the tab. After 5 minutes of no heartbeat the lock auto-expires.
    • They are unreachable → wait for the 5-minute timeout. After 5 minutes the lock is stale; open the session and the system gives you the lock.
  3. Do not start a new session for the same PO from /receiving/purchase-orders — the start endpoint detects the existing session and routes you to it (locked, read-only) rather than creating a duplicate.

⚠ The 5-minute window is the system's safety net, not your tool. If you intentionally walk away from a session for more than a minute, click back to the PO list — that releases the lock immediately. Don't tie up a PO from the lunch room.

4.5 Submitting for approval

Use when: You've counted everything you can count, exceptions (if any) are filed (per WMS-REC-002), and you're ready to hand the session to a manager for approval.

Prerequisites:

  • Session status is IN_PROGRESS (not already submitted).
  • At least one item has a quantityCounted > 0. The API rejects empty submits with "Cannot submit: no items counted".
  • You hold the lock. The API rejects with "Session is locked by another user" otherwise.

Steps:

  1. From the session page, tap the green Submit button at the bottom. (Disabled if summary.totalCounted === 0.)
  2. The Submit for Approval? modal opens with a summary:
    • Counted, Expected, and Variance (color-coded: green if exactly equal, yellow if overage, red if shortage)
    • {n} incomplete warning if any line has quantityCounted < quantityExpected
    • {n} overage warning if any line has quantityCounted > quantityExpected
  3. Review. If you see incompletes you forgot, tap Cancel and finish counting.
  4. (Optional) Assign a specific approver. The assignee dropdown is filtered server-side — only ADMIN and MANAGER users are valid. The API rejects others with "Assigned approver must be Admin or Manager". If you don't assign, all admins/managers get notified.
  5. Tap Submit.

Result:

  • Session status changes to SUBMITTED. submittedAt and submittedBy are set.
  • The lock is released (lockedBy and lockedAt cleared).
  • An audit_logs row is written with action: "SESSION_SUBMITTED", the totals, and the assignee.
  • Bell notifications are created for the assignee (or, if no assignee, all admins/managers).
  • The session page renders read-only with the status banner.
  • No InventoryUnit rows are created yet — that happens at approval time. See WMS-REC-003.

Common errors:

HTTPAPI messageWhat it means
400"Cannot submit: session is {status}"Already submitted, approved, rejected, or cancelled. Refresh.
400"Cannot submit: no items counted"No line has quantityCounted > 0. Either count something or cancel the session via WMS-REC-003.
400"Session is locked by another user"Lock was taken by someone else or you let it expire. See §4.4.
400"Assigned approver must be Admin or Manager"Selected assignee has a role of STAFF, READONLY, or SALES_REP. Pick a valid manager.
404"Session not found"Session ID is wrong or the session was deleted. Escalate per §8.

5. Reference

5.1 Receiving session status values

StatusMeaningWhat's allowed
IN_PROGRESSCounting is openScan, count, exception, submit, lock-related ops
SUBMITTEDAwaiting manager approvalView only; manager can approve/reject (WMS-REC-003)
APPROVEDInventory createdView only. Source of truth.
REJECTEDManager sent backView only; counter can Reopen & Recount (WMS-REC-003 §4.4) returning status to IN_PROGRESS
CANCELLEDSession abandonedView only; non-recoverable

Status transitions other than the above are blocked at the service layer. The API will return "Cannot {action}: session is {status}" for any operation incompatible with the current status.

5.2 Lock parameters

ParameterValueWhere set
Lock timeout5 minutes of silence after last heartbeatLOCK_TIMEOUT_MS in ReceivingService
Heartbeat interval60 seconds while session page is opensetInterval in pages/receiving/session.tsx
Lock takeoverAllowed automatically when lock is staleHandled by acquireLockAndReturn
Lock release on submitAtomic with status changesubmitForApproval clears lockedBy, lockedAt in the same transaction

5.3 Audit log action codes

The receiving session writes to audit_logs with entityType: "ReceivingSession" and entityId: <session id>. Action codes you'll encounter when investigating:

ActionWhen
SESSION_STARTEDNew session created
SESSION_RESUMEDExisting session re-opened with lock acquired
SCAN_SUCCESSBarcode matched a line
SCAN_FAILEDBarcode unknown or not on this PO (reason field: UNKNOWN_BARCODE or NOT_ON_PO)
QUANTITY_UPDATED+ / adjustment via /add or /batch
QUANTITY_SETExact quantity via /set
EXCEPTION_FILEDSee WMS-REC-002
SESSION_SUBMITTEDCounter handed off to manager
SESSION_APPROVED / SESSION_REJECTED / SESSION_REOPENEDSee WMS-REC-003
  • WMS-REC-002 — Receiving exceptions (damaged, wrong item, missing, overage, quality)
  • WMS-REC-003 — Submitting, approving, and rejecting a receiving session (manager flow)
  • WMS-REC-004 — Putaway from an approved session
  • WMS-REC-005 — Generating receiving labels (ZQ511 / DataWedge)
  • WMS-INV-006 — Backorder resolution (approval auto-triggers enqueueCheckBackorders per received variant)
  • WMS-AUD-003 — Receiving variance investigation

6. Audit & compliance

Every action in §4.1 through §4.5 writes one or more rows to audit_logs with entityType: "ReceivingSession". Rows are insert-only. The session itself, line-level counts, scan history (success and failure), and lock take-overs are all reconstructable from this table.

Counter responsibility:

  • Submit only what you physically counted. Manual entries that cannot be backed by physical observation belong in WMS-REC-002 as exceptions, not in the count.
  • Don't submit on behalf of another counter. The countedBy field is set at start time and is the system of record for who did the count.

Lead/manager periodic review (recommended weekly):

  • All SCAN_FAILED rows with reason: "NOT_ON_PO" for the past 7 days. A pattern (same SKU repeatedly arriving on POs that don't list it) means the PO source data is wrong — escalate to the buyer.
  • All sessions where submittedAt - createdAt > 4 hours. Long-running sessions are usually sessions someone forgot to submit, not sessions that took 4 hours to count.

7. Troubleshooting

SymptomCauseResolution
"No receiving location configured" (HTTP 400) on StartNo Location rows of type RECEIVING or STORAGE existHave an admin create a receiving location per WMS-INV-004 first. Counting cannot start until one exists.
The PO list is missing a PO that should be there60s cache hasn't expired, or the PO is in a status that's filtered outWait 60 seconds and refresh. If still missing, verify the PO exists in Inventory Planner.
"Session is locked by another user" and the named user is on lunchLock will timeout in <5 min from the most recent heartbeatWait for the 5-minute timeout, then open the session — lock auto-transfers. Or have them tap back on their device.
Scan beeps but the line doesn't incrementTC22 firmware sometimes drops Enter; the count submits via EnterRe-scan once, slowly. If still no increment, refresh the page (the scan was almost certainly registered server-side; refreshing reveals the true count). Don't count manually until you've confirmed via refresh.
"Version conflict" (HTTP 409) on every batch flushTwo devices on the same session, or a stuck queue on your deviceThe lock should have prevented this — escalate to IT. Never resolve a version conflict by retrying without verifying the line counts.
Session won't submit; Submit button stays disabledEither you don't hold the lock or totalCounted === 0Check the lock banner. Check that at least one line has a non-zero count.
"Assigned approver must be Admin or Manager" (HTTP 400) on SubmitThe assignee has role STAFF, READONLY, or SALES_REPPick a different assignee. If no manager is in the dropdown, the user list isn't loaded — refresh.
You finished counting but realize you double-scanned 30 itemsAudit log keeps the false scans, but the line quantityCounted is what gets approvedUse Set Exact (§4.3) to set the line to the correct number before submitting. Do not file an adjustment after approval — fix it now.
Counter submitted with the wrong countedBy (e.g., wrong login on shared TC22)The counter at start time is permanent in the auditManager rejects (WMS-REC-003), counter signs in correctly and reopens. Don't let this slide; the audit trail relies on accurate countedBy.

8. Escalation

  • PO not in Inventory Planner: Buyer + warehouse manager. Don't fabricate a session; the vendor traceability lives in IP.
  • Session won't unlock and the named user is unreachable: Wait the 5 minutes. If still stuck, IT on-call via #wms-support — they can clear the lock directly in the database with the session ID and timestamp logged.
  • Repeated Version conflict errors: IT on-call. Include the session ID and a screenshot. Don't keep retrying.
  • Suspected wrong-product receipt (vendor sent wrong SKU, in volume): Stop counting. File an exception per WMS-REC-002 with photos, then notify the buyer directly.
  • System-wide outage during a count in progress: WMS-AUD-004 paper-tally fallback. Record SKU, lot, qty, location on paper with your initials and a timestamp; sync into the session via §4.3 once the system returns.

9. Revision history

VersionDateAuthorChanges
1.0[DATE][NAME]Initial release. Covers session start (with the existing-session detection), the three counting paths (scan, +/−, Set Exact), the 5-minute lock with 60-second heartbeat and stale-lock takeover, the submit flow with assignee role validation. Documents exact API error strings from ReceivingService, exact UI labels from pages/receiving/session.tsx and pages/receiving/start.tsx, and audit log action codes written to audit_logs with entityType: "ReceivingSession". Cross-references WMS-REC-002 (exceptions), WMS-REC-003 (approval), WMS-REC-004 (putaway), WMS-REC-005 (labels), and WMS-INV-006 (backorder check on approval).