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
WorkTaskof typePUTAWAY) — 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 notADMINorMANAGER).
| Role | Start a session | Count | Submit | Approve / 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
Locationof typeRECEIVINGfirst, falling back to typeSTORAGE. If neither exists, the API returns"No receiving location configured"and the session cannot start.
Steps:
- 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. - 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
- Click the button.
- 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.
- The system creates the session and routes you to
/receiving/session/:sessionId.
Result:
- A
ReceivingSessionrow is created withstatus: "IN_PROGRESS",version: 1,lockedBy: <you>,lockedAt: <now>,countedBy: <you>. - One
ReceivingLineper expected item is created withquantityExpectedfrom the PO andquantityCounted: 0. - For variants on the PO that have neither a
upcnor abarcode, 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_logswithaction: "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:
- 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).
- Trigger the scan. The TC22 sends the barcode characters followed by Enter, which submits the form.
- The barcode is matched in this priority order: line
sku, linegeneratedBarcode, variantupc, variantbarcode, variantsku. - On success, the matched line increments by 1 and the scan feedback flashes
✓ {sku}. Phone vibrates briefly. The line'slastScannedAtandscanCountupdate. - 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.error | When | What you see / what to do |
|---|---|---|
NOT_ON_PO | Barcode matched a known variant but the variant isn't on this PO | Feedback: "{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_BARCODE | Barcode matched no variant in the system | Feedback: "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_LOCKED | Lock is held by another user, or session status changed mid-count | Feedback: "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
quantityCountedwill 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:
- 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.)
- The detail panel shows the line with
+and−buttons and a Set Exact option:+/−— single-unit add/subtract. Backed byPOST /receiving/:sessionId/add.- Set Exact — opens the modal, enter the new total. Backed by
POST /receiving/:sessionId/set.
- Updates are debounced and sent in batches via
POST /receiving/:sessionId/batchwith anexpectedVersion. If two devices changed the same session simultaneously, the second one to flush getsHTTP 409 Version conflict.
Result:
- The line's
quantityCountedupdates. The session'sversionincrements by 1 on each successful batch flush. - An
audit_logsrow is written withaction: "QUANTITY_UPDATED"(or"QUANTITY_SET"for explicit sets), thelineId, the previous and new quantities.
Common errors:
| HTTP | API message | What 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
lockedByto your user ID andlockedAtto now. - While the session page is open, the UI calls
POST /receiving/:sessionId/heartbeatevery 60 seconds to refreshlockedAt. - 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).
- You leave the session page (the UI calls
The lock UI states:
| State | What you see | What you can do |
|---|---|---|
| You hold the lock | Normal page; Submit button enabled | Count, scan, exception, submit. |
| Lock is yours, but session was submitted/approved/rejected from another tab | Page renders read-only with status banner | View only. To re-open a rejected session, see WMS-REC-003 §4.4. |
| Lock held by another active user | Red banner: Locked by {name} at the top; counting is disabled; submit hidden | View 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 page | Count normally. The previous holder, if they return, will see the locked-by-you banner. |
Steps if you see "Locked by {name}":
- Don't refresh repeatedly — the heartbeat is on a 60-second interval, refreshing won't free a lock held by an active user.
- 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.
- 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:
- From the session page, tap the green Submit button at the bottom. (Disabled if
summary.totalCounted === 0.) - 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 hasquantityCounted < quantityExpected{n}overage warning if any line hasquantityCounted > quantityExpected
- Review. If you see incompletes you forgot, tap Cancel and finish counting.
- (Optional) Assign a specific approver. The assignee dropdown is filtered server-side — only
ADMINandMANAGERusers are valid. The API rejects others with"Assigned approver must be Admin or Manager". If you don't assign, all admins/managers get notified. - Tap Submit.
Result:
- Session status changes to
SUBMITTED.submittedAtandsubmittedByare set. - The lock is released (
lockedByandlockedAtcleared). - An
audit_logsrow is written withaction: "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
InventoryUnitrows are created yet — that happens at approval time. See WMS-REC-003.
Common errors:
| HTTP | API message | What 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
| Status | Meaning | What's allowed |
|---|---|---|
IN_PROGRESS | Counting is open | Scan, count, exception, submit, lock-related ops |
SUBMITTED | Awaiting manager approval | View only; manager can approve/reject (WMS-REC-003) |
APPROVED | Inventory created | View only. Source of truth. |
REJECTED | Manager sent back | View only; counter can Reopen & Recount (WMS-REC-003 §4.4) returning status to IN_PROGRESS |
CANCELLED | Session abandoned | View 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
| Parameter | Value | Where set |
|---|---|---|
| Lock timeout | 5 minutes of silence after last heartbeat | LOCK_TIMEOUT_MS in ReceivingService |
| Heartbeat interval | 60 seconds while session page is open | setInterval in pages/receiving/session.tsx |
| Lock takeover | Allowed automatically when lock is stale | Handled by acquireLockAndReturn |
| Lock release on submit | Atomic with status change | submitForApproval 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:
| Action | When |
|---|---|
SESSION_STARTED | New session created |
SESSION_RESUMED | Existing session re-opened with lock acquired |
SCAN_SUCCESS | Barcode matched a line |
SCAN_FAILED | Barcode unknown or not on this PO (reason field: UNKNOWN_BARCODE or NOT_ON_PO) |
QUANTITY_UPDATED | + / − adjustment via /add or /batch |
QUANTITY_SET | Exact quantity via /set |
EXCEPTION_FILED | See WMS-REC-002 |
SESSION_SUBMITTED | Counter handed off to manager |
SESSION_APPROVED / SESSION_REJECTED / SESSION_REOPENED | See WMS-REC-003 |
5.4 Related SOPs
- 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
enqueueCheckBackordersper 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
countedByfield is set at start time and is the system of record for who did the count.
Lead/manager periodic review (recommended weekly):
- All
SCAN_FAILEDrows withreason: "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
| Symptom | Cause | Resolution |
|---|---|---|
"No receiving location configured" (HTTP 400) on Start | No Location rows of type RECEIVING or STORAGE exist | Have 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 there | 60s cache hasn't expired, or the PO is in a status that's filtered out | Wait 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 lunch | Lock will timeout in <5 min from the most recent heartbeat | Wait 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 increment | TC22 firmware sometimes drops Enter; the count submits via Enter | Re-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 flush | Two devices on the same session, or a stuck queue on your device | The 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 disabled | Either you don't hold the lock or totalCounted === 0 | Check the lock banner. Check that at least one line has a non-zero count. |
"Assigned approver must be Admin or Manager" (HTTP 400) on Submit | The assignee has role STAFF, READONLY, or SALES_REP | Pick 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 items | Audit log keeps the false scans, but the line quantityCounted is what gets approved | Use 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 audit | Manager 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 conflicterrors: 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
| Version | Date | Author | Changes |
|---|---|---|---|
| 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). |