Skip to main content

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/adjust API path used by the Adjust Quantity button

Out of scope:

  • Formal, audited inventory adjustments (the inventory_adjustments table 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.

RoleMove LocationAdjust QuantityMark DamagedView 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| > 10 units → notify the manager on duty in #warehouse-approvals before submitting
  • Marking damage on a unit with quantity > 10 → notify the manager on duty in #warehouse-approvals before 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:

  1. Physically verify the unit is at the expected source location before opening the app.
  2. Open the unit page (/inventory/:id). Confirm SKU, lot number (if tracked), quantity, and source location match what you intend to move.
  3. Click Move Location. The MoveModal opens in scan-first mode.
  4. 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."
  5. On the confirm step, leave Quantity blank to move the full unit, or enter a number to move a partial quantity.
  6. Click Move.
  7. Physically move the stock to the new location.

Result:

  • Full move (quantity blank or equal to unit qty): the unit's locationId updates in place. One inventory:unit_moved event is logged showing From and To locations.
  • Partial move (quantity less than unit qty): a new InventoryUnit is created at the destination with the moved quantity; the source unit's quantity decrements. Two linked events are logged with a shared correlationIdinventory:unit_split_source (renders as Split out) on the source unit and inventory: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}". For RESERVED or PICKED units, contact the assigned picker — these are tied to open orders.

⚠ The Move modal does not capture a reason. The API accepts a reason field but the current UI does not surface one. The inventory_events.payload.reason will be null for 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_adjustments row. The lightweight path here is for routine, small, isolated corrections only.

Prerequisites:

  • Unit status is AVAILABLE. Adjustments on RESERVED, PICKED, DAMAGED, IN_TRANSIT, or QUARANTINE units are blocked at the API layer.
  • You have recounted the physical stock at least twice and the counts agree.

Steps:

  1. Recount the physical stock. Write the number down. Recount. If the two numbers disagree, recount until they agree.
  2. Open the unit page. Click Adjust Quantity.
  3. 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.
  4. 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
  5. Submit.

Result:

  • The unit's quantity field updates directly. One inventory:unit_adjusted event (renders as Adjusted) is logged with previousQty, 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:

HTTPAPI messageWhat 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| > 10 must be accompanied by a Slack notification in #warehouse-approvals to 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, or QUARANTINE units.
  • 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:

  1. Photograph the damaged stock.
  2. Open the unit page. Click Mark Damaged.
  3. Enter the damaged quantity — leave blank to mark the entire unit, or enter a number for a partial damage.
  4. 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
  5. Submit.
  6. Physically move the damaged stock to the DAMAGED_INVENTORY location zone. (Damaged units retain their original locationId in 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. One inventory:unit_damaged event (renders as Damaged) is logged with the previous status, location, quantity, and reason.
  • Partial damage (quantity less than unit qty): a new InventoryUnit with status DAMAGED and the damaged quantity is created at the same location; the source unit's quantity decrements. Two linked events with a shared correlationId are 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:

HTTPAPI messageWhat 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-approvals to 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:

  1. Open the unit page (/inventory/:id).

  2. Scroll to the Activity section below the allocations table.

  3. 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)
  4. 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)
  5. For paginated history, click Load older at the bottom.

  6. For split events (partial moves and partial damages), the correlationId field 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.

StatusWhat it meansMoveAdjustDamageWhere set
AVAILABLESellable, on the shelfDefault on creation; restored on allocation release
RESERVEDHeld against an open order allocationSet automatically when an Allocation row is created
PICKEDPicked for an order, not yet shippedSet when picker confirms via TC22; see WMS-PICK-001
DAMAGEDPhysically unusable; not sellableSet by Mark Damaged; cannot be moved or adjusted
IN_TRANSITOutbound, on a carrierSet when shipping label is purchased; see WMS-SHIP-001
QUARANTINEOn regulatory or recall holdSet 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 count
  • Inventory audit
  • Received more
  • Found missing
  • Data correction
  • Other

For Mark Damaged:

  • Physical damage
  • Water damage
  • Expired
  • Quality issue
  • Customer return - damaged
  • Other

For Move Location: N/A — the Move modal does not capture a reason. See §4.1.

  • 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_events and return_events)
  • Regulatory traceability for cannabis/vape product (lot-tracked SKUs)

Manager weekly review (required):

  • Pull all inventory:unit_adjusted events with |delta| > 10 from 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-approvals at 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

SymptomCauseResolution
"Inventory unit {id} is not available - status is {status}" (HTTP 409) on Move or AdjustUnit status is RESERVED, PICKED, DAMAGED, IN_TRANSIT, or QUARANTINECheck 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 QUARANTINEDamage discovered during shipping goes through WMS-SHIP-004. Damage discovered during return goes through WMS-RET-002.
"Quantity cannot be negative" (HTTP 400) on AdjustUser entered a negative numberThe 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 qtySubmit with quantity field blank for a full damage.
"Insufficient quantity in unit {id}: requested {n}, available {x}" (HTTP 400) on partial MoveMoving more than is in the unitRecount 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 barcodeMove modal expects location barcodes only. Scan the bin/shelf label.
Activity feed doesn't show your most recent actionBrowser cache, or SSE channel didn't refreshRefresh 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 orderingSplit (move or damage) writes two events in a single transactionMatch 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

VersionDateAuthorChanges
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.