SOP: Inventory Unit Management & Activity Log
Document ID: WMS-INV-001
Version: 2.0
Effective date: 04/30/2026
Owner: Warehouse Operations Manager
Next review: [six months from effective date]
Applies to: All authenticated WMS users performing per-unit inventory operations on /inventory/:id
1. Purpose
This procedure governs how staff move, adjust quantity, and mark damage on individual inventory units in HQ WMS, and how managers and admins use the per-unit activity log to investigate discrepancies, resolve customer disputes, and prepare cycle-count reviews.
Every move, quantity adjustment, and damage event is logged to inventory_events with the user, timestamp, before/after state, location, and reason. The log is permanent (insert-only) and feeds into shrinkage reports, insurance claims, customer dispute resolution, and cycle-count variance investigation.
2. Scope
In scope:
- Per-unit operations performed from the unit detail page (
/inventory/:id) via the Move Location, Adjust Quantity, and Mark Damaged buttons - Reviewing the Activity feed below the allocations table on that page
- The lightweight
/inventory/:id/adjustAPI path used by the Adjust Quantity button
Out of scope:
- Formal, audited inventory adjustments (the
inventory_adjustmentstable with PENDING / APPROVED / REJECTED workflow, used by cycle-count approvals and high-impact corrections) — see WMS-INV-007 - Cycle-count campaigns (multi-unit, location-wide reconciliation) — see WMS-INV-002
- Floor count & new-SKU location discovery — see WMS-INV-003
- Receiving variances — see WMS-REC-002
- Customer return inbound handling — see WMS-RET-001 through WMS-RET-003
3. Roles & permissions
⚠ Honesty about enforcement. As of WMS API v[current], the move / adjust / damage endpoints (
POST /inventory/:id/move,/adjust,/damage) check authentication only. There is no role-based access control on these operations at the API layer — any authenticated user with a valid session can perform any of them. The control surface is the audit trail (inventory_events), not pre-emptive blocking.The table below describes operational expectations, not technically enforced permissions. Compliance is enforced through manager review of
inventory_events, not by the system refusing the action.
| Role | Move Location | Adjust Quantity | Mark Damaged | View Activity |
|---|---|---|---|---|
| READONLY | — | — | — | ✓ |
| STAFF | ✓ (any qty) | ✓ (see threshold below) | ✓ (see threshold below) | ✓ |
| MANAGER | ✓ | ✓ | ✓ | ✓ |
| ADMIN | ✓ | ✓ | ✓ | ✓ |
| SUPER_ADMIN | ✓ | ✓ | ✓ | ✓ |
Operational thresholds (manager review, not technically blocked):
- Quantity adjustments with
|delta| > 10units → notify the manager on duty in#warehouse-approvalsbefore submitting - Marking damage on a unit with
quantity > 10→ notify the manager on duty in#warehouse-approvalsbefore submitting - Any adjustment or damage with reason
Other→ managers will review weekly; explain in the reason text
If a more rigorous, system-enforced workflow is required (e.g., for cycle-count variance reconciliation, financial close, insurance claims), use the formal adjustment workflow in WMS-INV-007 instead — that path uses a separate inventory_adjustments table with PENDING → APPROVED/REJECTED status, distinct creator and approver, and signed audit rows.
4. Procedures
4.1 Moving a unit to a new location
Use when: Physically relocating stock, consolidating lots, reorganizing a zone, correcting a putaway error.
Prerequisites:
- Unit status is AVAILABLE. Reserved, picked, damaged, in-transit, and quarantined units cannot be moved (see §5.1).
- Destination location exists in the system and is active.
Steps:
- Physically verify the unit is at the expected source location before opening the app.
- Open the unit page (
/inventory/:id). Confirm SKU, lot number (if tracked), quantity, and source location match what you intend to move. - Click Move Location. The MoveModal opens in scan-first mode.
- Scan or type the destination barcode. The barcode is classified by
POST /scan:- Recognized location → moves to the confirm step.
- Unknown barcode → modal offers to quick-create a new location (see WMS-INV-004 §4.1).
- Product barcode → modal rejects with:
"That's a product barcode, not a location. Scan a bin or shelf label."
- On the confirm step, leave Quantity blank to move the full unit, or enter a number to move a partial quantity.
- Click Move.
- Physically move the stock to the new location.
Result:
- Full move (quantity blank or equal to unit qty): the unit's
locationIdupdates in place. Oneinventory:unit_movedevent is logged showing From and To locations. - Partial move (quantity less than unit qty): a new
InventoryUnitis created at the destination with the moved quantity; the source unit's quantity decrements. Two linked events are logged with a sharedcorrelationId—inventory:unit_split_source(renders as Split out) on the source unit andinventory:unit_split_dest(renders as Split in) on the new destination unit.
⚠ Do not move a unit whose status is anything other than
AVAILABLE. The API will return HTTP 409 with"Inventory unit {id} is not available - status is {status}". ForRESERVEDorPICKEDunits, contact the assigned picker — these are tied to open orders.
⚠ The Move modal does not capture a reason. The API accepts a
reasonfield but the current UI does not surface one. Theinventory_events.payload.reasonwill benullfor moves performed through the Move Location button. If you need to record why a move happened, add a note in the unit's notes field separately or escalate the move to a cycle count or formal adjustment.
4.2 Adjusting quantity (lightweight)
Use when: Physical count of a single, isolated unit doesn't match the system count, and the discrepancy is small.
Stop. If the discrepancy spans multiple units, a whole location, or a SKU across the warehouse, file a cycle count instead (WMS-INV-002). If the discrepancy will affect financial reporting, an insurance claim, or is the result of an approved cycle count, use the formal adjustment workflow (WMS-INV-007) — that path requires a manager approver and produces a signed
inventory_adjustmentsrow. The lightweight path here is for routine, small, isolated corrections only.
Prerequisites:
- Unit status is AVAILABLE. Adjustments on
RESERVED,PICKED,DAMAGED,IN_TRANSIT, orQUARANTINEunits are blocked at the API layer. - You have recounted the physical stock at least twice and the counts agree.
Steps:
- Recount the physical stock. Write the number down. Recount. If the two numbers disagree, recount until they agree.
- Open the unit page. Click Adjust Quantity.
- Enter the correct physical quantity in the New Quantity field. This is the new total, not a delta — if the system shows 12 and you have 10, enter
10, not-2. - Select a reason from the dropdown:
- Cycle count — discovered during a count, no other procedure ran
- Inventory audit — discovered during a manager audit
- Received more — receiving session under-reported
- Found missing — unit appeared after being marked short
- Data correction — known data error (e.g., import bug)
- Other — explain in a follow-up Slack message; managers review weekly
- Submit.
Result:
- The unit's
quantityfield updates directly. Oneinventory:unit_adjustedevent (renders as Adjusted) is logged withpreviousQty,newQty,delta, location, and reason. - If
delta > 0(quantity increased), the system enqueues a backorder check for that SKU automatically (enqueueCheckBackorders). See WMS-INV-006 for what happens next.
Common errors:
| HTTP | API message | What it means |
|---|---|---|
| 400 | "Quantity cannot be negative" | You entered a negative number. The field is the new total, not a delta. |
| 404 | "Inventory unit not found: {id}" | The unit was deleted or merged since you opened the page. Refresh. |
| 409 | "Inventory unit {id} is not available - status is {status}" | Status isn't AVAILABLE. Don't repeat the request — investigate why. |
⚠ Adjustments with
|delta| > 10must be accompanied by a Slack notification in#warehouse-approvalsto the manager on duty before submitting. The system will not block the action; manager review is the control.
4.3 Marking damaged
Use when: Stock is physically unusable and cannot be sold — crushed, contaminated, expired, water-damaged, or returned in unsellable condition.
Prerequisites:
- Unit status is AVAILABLE or RESERVED. The API blocks damage on
PICKED,DAMAGED,IN_TRANSIT, orQUARANTINEunits. - A photo of the damaged stock has been captured (phone camera is fine; save to the warehouse shared folder per WMS-AUD-002 §4).
Steps:
- Photograph the damaged stock.
- Open the unit page. Click Mark Damaged.
- Enter the damaged quantity — leave blank to mark the entire unit, or enter a number for a partial damage.
- Select a reason from the dropdown:
- Physical damage — crushing, dropping, impact
- Water damage — flooding, leak, condensation
- Expired — past expiration date (use WMS-INV-005 first for lot-tracked product)
- Quality issue — manufacturing defect, contamination, foreign material
- Customer return - damaged — discovered damaged during return intake (cross-reference the customer return number in notes)
- Other — explain; managers review weekly
- Submit.
- Physically move the damaged stock to the DAMAGED_INVENTORY location zone. (Damaged units retain their original
locationIdin the system — the physical move is for staff visibility, not a system move.)
Result:
- Full damage (quantity blank or equal to unit qty): the unit's status becomes
DAMAGED. Oneinventory:unit_damagedevent (renders as Damaged) is logged with the previous status, location, quantity, and reason. - Partial damage (quantity less than unit qty): a new
InventoryUnitwith statusDAMAGEDand the damaged quantity is created at the same location; the source unit's quantity decrements. Two linked events with a sharedcorrelationIdare logged —inventory:unit_damaged_source(renders as Partial damage) on the original unit,inventory:unit_damaged_dest(renders as Damaged (split)) on the new damaged unit.
Common errors:
| HTTP | API message | What it means |
|---|---|---|
| 400 | "Cannot mark {qty} as damaged, only {available} available. Use markDamaged for full unit." | You requested a partial damage equal to or greater than the unit's full quantity. Submit with quantity blank for a full damage. |
| 400 | "Quantity must be positive" | Damage quantity was zero or negative. |
| 409 | "Only AVAILABLE or RESERVED inventory can be marked damaged" | Status is PICKED, DAMAGED, IN_TRANSIT, or QUARANTINE. For damage discovered during shipping, see WMS-SHIP-004. |
⚠ Expired or recalled product requires the lot/expiry handling procedure in WMS-INV-005 before marking damaged in this system. The destruction-with-witness step and the regulatory-hold step both happen there. Do not mark expired cannabis or vape product as damaged here without first completing WMS-INV-005.
⚠ Damaging a unit with quantity > 10 must be accompanied by a Slack notification in
#warehouse-approvalsto the manager on duty before submitting.
4.4 Reviewing unit history (Activity feed)
Use when: Investigating a discrepancy, preparing a cycle-count review, responding to a customer claim, tracing a shrinkage incident, building an insurance claim.
Steps:
-
Open the unit page (
/inventory/:id). -
Scroll to the Activity section below the allocations table.
-
Events render newest-first with these labels:
Event type (DB) Activity feed label inventory:unit_movedMoved inventory:unit_split_sourceSplit out inventory:unit_split_destSplit in inventory:unit_adjustedAdjusted inventory:unit_damagedDamaged inventory:unit_damaged_sourcePartial damage inventory:unit_damaged_destDamaged (split) -
Each entry shows:
- The label and an icon
- The user who performed it (or System for automated events)
- Quantity involved and before/after state where applicable
- From and To locations for moves and splits
- Relative timestamp; hover to see the exact UTC date/time
- The reason text if one was captured (note: moves never have a reason — see §4.1 warning)
-
For paginated history, click Load older at the bottom.
-
For split events (partial moves and partial damages), the
correlationIdfield links the two halves. The activity entry includes the linked unit ID — click through to the linked unit's page to see the other half of the transaction.
5. Reference
5.1 Inventory status values
The InventoryStatus enum has six values. Each has different operational rules.
| Status | What it means | Move | Adjust | Damage | Where set |
|---|---|---|---|---|---|
AVAILABLE | Sellable, on the shelf | ✓ | ✓ | ✓ | Default on creation; restored on allocation release |
RESERVED | Held against an open order allocation | — | — | ✓ | Set automatically when an Allocation row is created |
PICKED | Picked for an order, not yet shipped | — | — | — | Set when picker confirms via TC22; see WMS-PICK-001 |
DAMAGED | Physically unusable; not sellable | — | — | — | Set by Mark Damaged; cannot be moved or adjusted |
IN_TRANSIT | Outbound, on a carrier | — | — | — | Set when shipping label is purchased; see WMS-SHIP-001 |
QUARANTINE | On regulatory or recall hold | — | — | — | Set manually by manager during recall; see WMS-INV-005 |
A unit's status is the primary gate for what operations are allowed. The API errors above all flow from these rules.
5.2 Approved reason codes (per UI dropdown, exact strings)
The reason field is enforced as a free-text string at the API layer, but the UI offers fixed dropdown choices. Always use the dropdown values for consistency in reports.
For Adjust Quantity:
Cycle countInventory auditReceived moreFound missingData correctionOther
For Mark Damaged:
Physical damageWater damageExpiredQuality issueCustomer return - damagedOther
For Move Location: N/A — the Move modal does not capture a reason. See §4.1.
5.3 Related SOPs
- WMS-INV-002 — Cycle count campaigns (multi-unit reconciliation)
- WMS-INV-003 — Floor count & location discovery (new-SKU mapping)
- WMS-INV-004 — Location management (creating, deactivating locations)
- WMS-INV-005 — Expired & recalled product handling (lot/quarantine path)
- WMS-INV-006 — Backorder resolution (post-adjustment-increase flow)
- WMS-INV-007 — Formal inventory adjustments (audited approval workflow)
- WMS-RET-002 — Customer return inspection (where damaged-on-return originates)
- WMS-SHIP-004 — Shipping damage & in-transit issues
- WMS-AUD-002 — Shrinkage investigation (uses this SOP's audit trail)
6. Audit & compliance
Every operation in §4.1–4.3 writes one or two immutable rows to inventory_events. Rows are insert-only — never updated, never deleted. The activity log is the system of record for:
- Monthly shrinkage reporting
- Quarterly cycle-count variance review
- Insurance claims on damaged product (the photo and the event row together)
- Customer dispute resolution (cross-referenced with
fulfillment_eventsandreturn_events) - Regulatory traceability for cannabis/vape product (lot-tracked SKUs)
Manager weekly review (required):
- Pull all
inventory:unit_adjustedevents with|delta| > 10from the past 7 days. - Pull all
inventory:unit_damaged*events with quantity > 10. - Pull all events where
reason = "Other". - For each, confirm the Slack notification was posted to
#warehouse-approvalsat the time. If a pattern emerges (same SKU, same user, repeated adjustments), open a cycle-count task for that location per WMS-INV-002.
Quarterly governance (Warehouse Operations Manager):
- Variance trend by user
- Variance trend by SKU
- Variance trend by location
Rising trends in any dimension warrant investigation per WMS-AUD-002.
7. Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
"Inventory unit {id} is not available - status is {status}" (HTTP 409) on Move or Adjust | Unit status is RESERVED, PICKED, DAMAGED, IN_TRANSIT, or QUARANTINE | Check active allocations on the same page. For RESERVED/PICKED, contact the assigned picker. For DAMAGED/IN_TRANSIT/QUARANTINE, the operation is not appropriate — review the Activity feed to see how the unit reached this state. |
"Only AVAILABLE or RESERVED inventory can be marked damaged" (HTTP 409) | Unit is already PICKED, DAMAGED, IN_TRANSIT, or QUARANTINE | Damage discovered during shipping goes through WMS-SHIP-004. Damage discovered during return goes through WMS-RET-002. |
"Quantity cannot be negative" (HTTP 400) on Adjust | User entered a negative number | The field is the new total, not a delta. Enter the recounted physical quantity. |
"Cannot mark {n} as damaged, only {x} available" (HTTP 400) | Partial damage qty ≥ unit qty | Submit with quantity field blank for a full damage. |
"Insufficient quantity in unit {id}: requested {n}, available {x}" (HTTP 400) on partial Move | Moving more than is in the unit | Recount physically. If system count is wrong, run §4.2 first to correct, then move. |
"That's a product barcode, not a location. Scan a bin or shelf label." (in MoveModal) | Scanned a product UPC/SKU instead of a location barcode | Move modal expects location barcodes only. Scan the bin/shelf label. |
| Activity feed doesn't show your most recent action | Browser cache, or SSE channel didn't refresh | Refresh the page. If still missing after refresh, do not repeat the action — escalate per §8. The action may have written to the DB while the response was lost. |
| Two events have the same timestamp but unclear ordering | Split (move or damage) writes two events in a single transaction | Match by correlationId in the Activity feed metadata; the source/dest pair is one logical operation. |
8. Escalation
- Procedure question during an operation: Don't pause for the SOP — ask the lead on duty. Update this SOP afterward if the answer wasn't here.
- System error not in §7: IT on-call via
#wms-support. Include the unit ID, exact error message, and a screenshot. Do not retry destructive operations. - Suspected shrinkage or theft: Warehouse Operations Manager directly. Do not discuss in Slack.
- Possible data corruption (e.g., Activity feed missing events you know occurred): IT on-call. Tag the database; do not perform further operations on the affected unit until cleared.
9. Revision history
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | [DATE] | [NAME] | Initial release covering move / adjust / damage operations and the unit activity feed. |
| 2.0 | [DATE] | [NAME] | Material revision after codebase audit. Corrected role table to reflect actual API enforcement (no role gating on these endpoints — audit trail is the control, not pre-emptive blocking). Added QUARANTINE to the status enum (six statuses, was four). Renamed buttons to match the UI verbatim: Move Location, Adjust Quantity, Mark Damaged. Replaced invented reason codes with the exact dropdown options in apps/web/src/pages/inventory/[id].tsx. Added explicit warning that the Move modal does not capture a reason. Distinguished this lightweight /adjust path from the formal inventory_adjustments workflow now covered in WMS-INV-007. Replaced fabricated error strings with the exact messages thrown by @wms/domain. Added split-event correlation explanation. Documented automatic backorder check on quantity-increasing adjustment. Expanded §6 with concrete weekly and quarterly manager review tasks. Added §5.1 status table showing operation-vs-status compatibility. |