Overview
Stock Movements are the heart of inventory traceability. Every change to stock—whether opening balance, transfer, sale, or adjustment—is recorded as aStockMovement with full details: who, when, why, and how much.
All movements support multi-UOM transactions: you can receive in boxes but track in units, or sell in grams but store in kilograms.
Movement Types
SushiGo supports seven movement reasons, each with specific directional semantics:OPENING_BALANCE - Initial Stock
Registers initial inventory at a location
Use case: First-time stock registration, new location setup
Cost impact: Updates
weighted_avg_cost and last_unit_cost
Example: Register 50kg rice at Main Warehouse with cost $2.50/kg
TRANSFER - Location to Location
Moves stock between locations within or across operating units
Use case: Daily prep transfers (MAIN → KITCHEN), inter-branch transfers
Cost impact: No cost update (preserves weighted average) Example: Transfer 10kg rice from Main Warehouse to Kitchen
RETURN - Reverse Movement
Returns stock from customer or location back to source
Use case: Customer returns, unused event stock returning to branch
Cost impact: No cost update Example: Return 5kg unused rice from event to Main Warehouse
SALE - Customer Sale
Records sale to customer (decreases inventory)
Use case: Customer purchases, POS integration
Cost impact: No cost update (uses current weighted average for COGS) Example: Sell 2kg rice to customer at $5/kg
CONSUMPTION - Internal Use
Internal consumption without sale (meals, samples, spoilage)
Use case: Employee meals, tastings, damaged/expired items
Cost impact: No cost update (expense tracking) Example: 0.5kg rice spoiled due to contamination
ADJUSTMENT - Manual Correction
Manual stock adjustment (up or down)
Use case: Inventory corrections, system adjustments
Cost impact: Optional (can update cost on upward adjustments) Example: Adjust inventory +2kg after finding misplaced stock
COUNT_VARIANCE - Physical Count
Adjustment from physical inventory count
Use case: Daily/weekly/monthly physical counts
Cost impact: No cost update (variance tracking) Example: Physical count shows 48kg actual vs 50kg system = -2kg variance
Movement Status
Each movement has a status that controls whether it affects stock:- DRAFT
- POSTED
- REVERSED
Created but not posted. Does not affect stock. Can be edited or deleted.
Movement Fields
Source location (null for entries like OPENING_BALANCE)
Target location (null for exits like SALE)
Item variant being moved
User who created the movement (auto-populated)
Quantity in base UOM (auto-converted from transaction UOM)
Movement reason code
DRAFT, POSTED, or REVERSED
External reference number (PO, invoice, ticket, etc.)
ID of related entity (sale, purchase order, etc.)
Polymorphic type (Sale, PurchaseOrder, etc.)
Additional notes or explanation
Movement metadata:
original_qty: Quantity in transaction UOMoriginal_uom: Transaction UOM codeunit_cost: Cost per transaction unitbase_cost: Cost per base unitsale_price: Sale price per transaction unitconversion_factor: UOM conversion factor applied
When the movement was posted (auto-set on posting)
API Endpoints
Register Opening Balance
Opening balance automatically posts and updates
stock.on_hand and item_variant.avg_unit_costRegister Stock Out (Sale or Consumption)
Register Transfer
List Movements
Filter by location (from OR to)
Filter by item variant
Filter by movement reason
Filter by status (DRAFT, POSTED, REVERSED)
Filter movements on or after this date
Filter movements on or before this date
Results per page
Real-World Examples
Example 1: Opening Balance with Cost
Register initial stock of salmon at main warehouse:stock.on_hand→ 20.0 kgstock.weighted_avg_cost→ $18.50/kgitem_variant.avg_unit_cost→ $18.50item_variant.last_unit_cost→ $18.50
Example 2: Multi-UOM Transfer
Transfer rice from warehouse (in kg) to kitchen (prep counted in g): Warehouse has: 50 kg riceTransfer: 2000 g (2 kg)
- Validates conversion: 2000g → 2kg (factor: 0.001)
- Checks availability: 50kg available ≥ 2kg ✓
- Updates stock:
- Location 1: 50kg → 48kg
- Location 2: 0kg → 2kg
- Records in
meta:
Example 3: Sale with Profit Tracking
Sell sushi roll with automatic profit calculation:avg_unit_cost: $8.50/unit
stock.on_handdecreases by 8 unitsmeta.unit_cost: $8.50meta.sale_price: $15.00meta.profit_margin: 15 - $8.50)- Total profit: 8 × 52.00
Example 4: Spoilage Tracking
Record spoiled items for loss analysis:- Stock decreases by 1.5 kg
- Cost recorded in
meta.unit_costfor loss report - Movement tagged with CONSUMPTION for spoilage analysis
UOM Conversion Flow
When a movement uses a different UOM than the variant’s base:Store Both
qtyfield: base quantity (for stock updates)meta.original_qty,meta.original_uom: transaction quantity
- Variant base UOM:
UNIT(individual bottles) - Receipt UOM:
BOX(24 units per box) - Conversion: 1 BOX = 24 UNIT (factor: 24)
base_qty = 5 × 24 = 120 unitsmeta.original_qty = 5meta.original_uom = "BOX"meta.conversion_factor = 24
Movement Lines (Multi-Line)
For complex movements with multiple items, useStockMovementLine:
Business Rules
Atomic Operations
All stock updates are transactional (rollback on error)
Availability Validation
Outbound movements validate available stock before posting
Cost Preservation
Only OPENING_BALANCE updates costs; transfers preserve weighted average
Immutable Posted
Posted movements cannot be edited, only reversed
Movement Direction Rules
| Reason | from_location | to_location | Validation |
|---|---|---|---|
| OPENING_BALANCE | null | required | N/A |
| TRANSFER | required | required | from ≠ to, same branch check |
| RETURN | required | required | N/A |
| SALE | required | null | available >= qty |
| CONSUMPTION | required | null | available >= qty |
| ADJUSTMENT | optional | optional | at least one location |
| COUNT_VARIANCE | optional | optional | at least one location |
Workflows
Workflow 1: Daily Prep Transfer
Workflow 2: Physical Count Adjustment
Workflow 3: Sale with Profitability
Error Handling
Common errors and solutions:Insufficient Stock (400)
Insufficient Stock (400)
Error: “Insufficient stock. Available: 5, Requested: 10”Causes:
- Stock reserved by pending orders
- Recent consumption not reflected
- Check
stock.available(not juston_hand) - Transfer from another location
- Reduce requested quantity
No Conversion Found (400)
No Conversion Found (400)
Error: “No active conversion found from BOX to KG”Causes:
- Missing UOM conversion record
- Conversion marked as inactive
- Create conversion:
/api/v1/uom-conversions - Use base UOM instead
- Activate existing conversion
Location Not Found (422)
Location Not Found (422)
Error: “The selected inventory location does not exist”Causes:
- Invalid location ID
- Location soft-deleted
- Verify location ID exists
- Check
is_active=true - Restore deleted location if needed
Invalid Movement Direction (422)
Invalid Movement Direction (422)
Error: “SALE movements require from_location only”Causes:
- Wrong location field for reason type
- SALE/CONSUMPTION: only
from_location - OPENING_BALANCE: only
to_location - TRANSFER/RETURN: both locations required
Database Schema
stock_movements Table
| Column | Type | Constraints |
|---|---|---|
| id | bigint | PK, auto-increment |
| from_location_id | bigint | FK → inventory_locations.id, NULLABLE |
| to_location_id | bigint | FK → inventory_locations.id, NULLABLE |
| item_variant_id | bigint | FK → item_variants.id, NOT NULL |
| user_id | bigint | FK → users.id, NULLABLE |
| qty | decimal(10,4) | NOT NULL (in base UOM) |
| reason | enum | NOT NULL |
| status | enum | DEFAULT ‘POSTED’ |
| reference | varchar(100) | NULLABLE |
| related_id | bigint | NULLABLE |
| related_type | varchar(255) | NULLABLE |
| notes | text | NULLABLE |
| meta | jsonb | DEFAULT '' |
| posted_at | timestamp | NULLABLE |
| created_at | timestamp | |
| updated_at | timestamp |
idx_stock_movements_from_locationon (from_location_id)idx_stock_movements_to_locationon (to_location_id)idx_stock_movements_item_varianton (item_variant_id)idx_stock_movements_reasonon (reason)idx_stock_movements_posted_aton (posted_at DESC)
- Check:
(from_location_id IS NOT NULL) OR (to_location_id IS NOT NULL) - Check:
qty > 0
Next Steps
Unit Conversions
Set up UOM conversions for multi-unit transactions
Items & Variants
Define items and variants to move
Inventory Locations
Configure locations for stock storage