# Inventory Request

## Overview

Inventory operations are built around two complementary patterns:

1. **Atomic commands** (`receiveStock`, `issueStock`) for immediate stock changes triggered by external modules (Purchase, Sales)
2. **StockAdjustment** with approval workflow for internal adjustments (CORRECTION, SCRAP, BLOCK, UNBLOCK)

Both patterns create **InventoryLedger** entries as the immutable source of truth. StockLevel is maintained as a computed cache of the ledger.

**InventoryLedger -- Source of Truth**

InventoryLedger is an INSERT-only, immutable table that records every physical stock change. Each entry carries:

| Field | Description |
|---|---|
| `sourceType` | GOODS_RECEIPT, GOODS_ISSUE, BLOCK, UNBLOCK, CORRECTION, SCRAP |
| `sourceId` | Reference to the originating document (e.g., PurchaseOrder ID, StockAdjustment ID) |
| `dcIndicator` | Debit/Credit indicator (DR or CR) for accounting integration |
| `amount` | Always positive quantity; direction determined by `dcIndicator` |
| `itemId` | The item affected |
| `storageLocationId` | The location where the change occurs |
| `executedAt` | Timestamp when the physical change occurred |
| `lotId` | Optional lot/batch reference |
| `serialNumberId` | Optional serial number reference |
| `unitCost` | Unit cost at time of entry (for valuation) |

InventoryLedger has no `updatedAt` -- only `createdAt` -- to enforce immutability. Once created, entries are permanent and cannot be modified or deleted.

**StockLevel -- Computed Cache**

StockLevel is a per-item, per-location summary maintained as a cache derived from InventoryLedger entries. It provides fast access to current stock positions without scanning the full ledger.

| StockLevel Field | Source |
|---|---|
| `onHand` | Sum of Ledger DR entries (GOODS_RECEIPT, CORRECTION INCREASE) minus CR entries (GOODS_ISSUE, CORRECTION DECREASE, SCRAP) |
| `reserved` | Set by Sales/external reservation commands (outside Ledger) |
| `incoming` | Set by Purchase/external expected receipt signals (outside Ledger) |
| `blocked` | Sum of Ledger BLOCK entries minus UNBLOCK entries |
| `available` | Computed: `onHand - reserved - blocked` |

**Atomic Commands Pattern**

Atomic commands provide immediate, single-step stock changes. There is no draft-then-confirm lifecycle -- the command executes in one call, creating InventoryLedger entries and updating StockLevel atomically.

| Command | Ledger Effect | StockLevel Effect |
|---|---|---|
| `receiveStock` (`toBlocked=false`) | DR entry (GOODS_RECEIPT) | onHand +, available + |
| `receiveStock` (`toBlocked=true`) | DR entry (GOODS_RECEIPT) + CR entry (BLOCK) + DR entry (BLOCK) | onHand +, blocked +, available unchanged |
| `issueStock` (`fromBlocked=false`) | CR entry (GOODS_ISSUE) | onHand -, available - |
| `issueStock` (`fromBlocked=true`) | CR entry (GOODS_ISSUE) + CR entry (UNBLOCK) | onHand -, blocked - |

- **receiveStock**: Receives stock into a destination location. Creates a GOODS_RECEIPT ledger entry. By default (`toBlocked=false`), stock is immediately available. When `toBlocked=true`, the command also creates BLOCK entries (CR + DR) to immediately quarantine the received stock for quality inspection (SAP QI Stock pattern). The stock appears in `onHand` and `blocked` but not in `available`. A subsequent StockAdjustment (UNBLOCK or SCRAP) resolves the inspection.
- **issueStock**: Issues stock out of a source location. Creates a GOODS_ISSUE ledger entry. By default (`fromBlocked=false`), validates that sufficient available stock exists before executing. When `fromBlocked=true`, issues from blocked stock instead (e.g., returning inspection-failed goods to supplier), also creating an UNBLOCK ledger entry to decrement the blocked quantity.

**StockAdjustment Workflow**

StockAdjustment provides an approval workflow for internal adjustments. It follows a multi-step lifecycle:

| adjustmentType | Description |
|---|---|
| CORRECTION | Adjust stock quantity to reconcile system vs. physical (INCREASE or DECREASE) |
| SCRAP | Remove damaged, expired, or defective stock from inventory |
| BLOCK | Restrict stock availability at a location (e.g., quality hold) |
| UNBLOCK | Release previously blocked stock back to available inventory |

**Status lifecycle**: DRAFT → SUBMITTED → CONFIRMED (or REJECTED → DRAFT for resubmission)

```
DRAFT --> submit --> SUBMITTED --> confirm --> CONFIRMED
  |                     |
  v                     v
cancel --> CANCELLED  reject --> REJECTED --> update --> DRAFT (resubmit)
```

Each StockAdjustment has:
- `adjustmentType` (CORRECTION, SCRAP, BLOCK, UNBLOCK)
- `status` (DRAFT, SUBMITTED, REJECTED, CONFIRMED, CANCELLED)
- `reasonCode` (required -- explains why the adjustment is being made)
- One or more **StockAdjustmentLines**, each specifying: `itemId`, `storageLocationId`, `quantity`, `adjustmentDirection` (INCREASE or DECREASE), and optional `lotId`, `serialNumberId`, `unitCost`

Ledger entries and StockLevel updates are created only when a StockAdjustment reaches CONFIRMED status.

| adjustmentType | Ledger Entry on Confirm | StockLevel Effect |
|---|---|---|
| CORRECTION (INCREASE) | DR entry (CORRECTION) | onHand +, available + |
| CORRECTION (DECREASE) | CR entry (CORRECTION) | onHand -, available - |
| SCRAP | CR entry (SCRAP) | onHand -, available - |
| BLOCK | DR entry (BLOCK) | blocked +, available - |
| UNBLOCK | CR entry (UNBLOCK) | blocked -, available + |

## Business Purpose

- Provide atomic, immediately-consistent stock commands for external operations -- no two-phase draft-then-confirm for purchase receipts or sales issues
- Maintain approval workflow for internal adjustments via StockAdjustment, supporting organizational controls for corrections, scrap, and quality holds
- Immutable audit trail via InventoryLedger with DR/CR indicator for seamless accounting integration
- Inspection stock tracked as blocked inventory via `receiveStock(toBlocked=true)` (SAP QI Stock pattern), resolved through StockAdjustment UNBLOCK, StockAdjustment SCRAP, or `issueStock(fromBlocked=true)`
- Enable traceability from any ledger entry back to its source document via `sourceType`/`sourceId`
- Feed downstream processes (inventory valuation, lot/batch tracking, serial number tracking) with ledger data

## Process Flow

```mermaid
flowchart TD
    subgraph External["External Module Operations"]
        PO[postGoodsReceipt - Purchase] --> RCV["receiveStock (toBlocked param)"]
        SO[postGoodsIssue - Sales] --> ISS[issueStock]
    end

    subgraph Atomic["Atomic Command Execution"]
        RCV --> LED1[Create InventoryLedger entries]
        ISS --> LED3[Create InventoryLedger entry]
        LED1 --> SL1[Update StockLevel]
        LED3 --> SL3[Update StockLevel]
    end

    subgraph Inspection["Inspection Resolution"]
        RCV -.->|"toBlocked=true: Stock blocked"| INSP[Inspect goods]
        INSP -->|Pass| UNB[StockAdjustment: UNBLOCK]
        INSP -->|Fail| SCR[StockAdjustment: SCRAP]
    end

    subgraph Adjustment["StockAdjustment Workflow"]
        UNB --> WF
        SCR --> WF
        ADJ[createStockAdjustment] --> WF[DRAFT]
        WF -->|submit| SB[SUBMITTED]
        SB -->|confirm| CF[CONFIRMED]
        SB -->|reject| RJ[REJECTED]
        RJ -->|update| WF
        CF --> LED4[Create InventoryLedger entries]
        LED4 --> SL4[Update StockLevel]
    end

```

## Scenario Patterns

- **Goods Receipt**: The Purchase module calls `receiveStock` after goods arrive. A GOODS_RECEIPT ledger entry (DR) is created, increasing `onHand` and `available` at the destination location. No draft or approval step is needed -- the stock is immediately available.

- **Goods Receipt with Inspection**: The Purchase module calls `receiveStock(toBlocked=true)` for items requiring quality checks. Three ledger entries are created: GOODS_RECEIPT (DR, increases `onHand`), BLOCK (CR), and BLOCK (DR, increases `blocked`). The stock appears in `onHand` but not in `available`. After inspection, a StockAdjustment of type UNBLOCK (if passed) or SCRAP (if failed) goes through the approval workflow to resolve the blocked stock.

- **Goods Issue**: The Sales module calls `issueStock` when shipping goods. A GOODS_ISSUE ledger entry (CR) is created, decreasing `onHand` and `available` at the source location. The command validates that sufficient available stock exists before executing.

- **Correction**: A discrepancy is found between system records and physical stock. A StockAdjustment with `adjustmentType=CORRECTION` and a `reasonCode` is created. Each line specifies `adjustmentDirection` (INCREASE or DECREASE) and `quantity`. After submission and confirmation, CORRECTION ledger entries adjust `onHand` accordingly.

- **Scrap**: Damaged or defective stock is identified for disposal. A StockAdjustment with `adjustmentType=SCRAP` and a `reasonCode` is created. After submission and confirmation, SCRAP ledger entries (CR) reduce `onHand` and `available`, and reduce inventory valuation.

- **Stock Block**: Suspect stock needs to be held from use. A StockAdjustment with `adjustmentType=BLOCK` and a `reasonCode` is created. After submission and confirmation, BLOCK ledger entries (DR) increase `blocked` and reduce `available`.

- **Stock Unblock**: Previously blocked stock is cleared after investigation. A StockAdjustment with `adjustmentType=UNBLOCK` and a `reasonCode` is created. After submission and confirmation, UNBLOCK ledger entries (CR) decrease `blocked` and restore `available`.

- **Count Variance**: Count variances discovered during physical inventory are recorded as StockAdjustment(CORRECTION), going through the standard approval workflow before creating ledger entries.

**Inspection Flow Example**

```
1. postGoodsReceipt calls receiveStock(100, toBlocked=true)
   -> Ledger: GOODS_RECEIPT DR +100, BLOCK CR +100, BLOCK DR +100
   -> StockLevel: onHand=100, blocked=100, available=0

2. Inspector examines goods: 80 pass, 20 fail

3. Release 80 passing units:
   StockAdjustment(UNBLOCK, 80) submit -> confirm
   -> Ledger: UNBLOCK DR +80, UNBLOCK CR -80
   -> StockLevel: onHand=100, blocked=20, available=80

4. Application decides how to handle the 20 failing units (3 patterns):

   Pattern A: Scrap directly from blocked
     StockAdjustment(SCRAP, 20, fromStockCategory=BLOCKED) submit -> confirm
     -> Ledger: SCRAP CR -20, UNBLOCK CR -20
     -> StockLevel: onHand=80, blocked=0, available=80

   Pattern B: Return to supplier from blocked
     issueStock(20, fromBlocked=true, sourceId=returnDoc)
     -> Ledger: GOODS_ISSUE CR -20, UNBLOCK CR -20
     -> StockLevel: onHand=80, blocked=0, available=80

   Pattern C: Hold (do nothing)
     -> StockLevel unchanged: onHand=100, blocked=20, available=80
```

**Balance Verification**

The ledger serves as the authoritative source. StockLevel can be verified against the ledger at any time:

```sql
-- Verify onHand from ledger
SELECT
  item_id,
  storage_location_id,
  SUM(CASE WHEN dc_indicator = 'DR' AND source_type IN ('GOODS_RECEIPT', 'CORRECTION') THEN amount ELSE 0 END)
  - SUM(CASE WHEN dc_indicator = 'CR' AND source_type IN ('GOODS_ISSUE', 'CORRECTION', 'SCRAP') THEN amount ELSE 0 END)
  AS computed_on_hand
FROM inventory_ledger
GROUP BY item_id, storage_location_id;

-- Verify blocked from ledger
SELECT
  item_id,
  storage_location_id,
  SUM(CASE WHEN source_type = 'BLOCK' THEN amount ELSE 0 END)
  - SUM(CASE WHEN source_type = 'UNBLOCK' THEN amount ELSE 0 END)
  AS computed_blocked
FROM inventory_ledger
GROUP BY item_id, storage_location_id;
```

**External Integration**

| Caller | Inventory Command |
|---|---|
| `postGoodsReceipt` (Purchase) | `receiveStock` (`toBlocked` is an optional parameter; defaults to false) |
| `postGoodsIssue` (Sales) | `issueStock` |

## Test Cases

- `receiveStock` creates a GOODS_RECEIPT ledger entry and increases onHand and available
- `receiveStock(toBlocked=true)` creates GOODS_RECEIPT and BLOCK ledger entries; onHand and blocked increase but available does not
- `issueStock` creates a GOODS_ISSUE ledger entry and decreases onHand and available
- `issueStock` is rejected if available stock is insufficient
- StockAdjustment follows the DRAFT → SUBMITTED → CONFIRMED lifecycle
- StockAdjustment can be rejected at SUBMITTED (transitions to REJECTED)
- A REJECTED StockAdjustment can be updated (transitions back to DRAFT) and resubmitted
- StockAdjustment can be cancelled at DRAFT (transitions to CANCELLED)
- StockAdjustment requires a `reasonCode`
- StockAdjustment CORRECTION (INCREASE) creates DR ledger entry on confirm; increases onHand and available
- StockAdjustment CORRECTION (DECREASE) creates CR ledger entry on confirm; decreases onHand and available
- StockAdjustment SCRAP creates CR ledger entry on confirm; decreases onHand and available
- StockAdjustment BLOCK creates DR ledger entry on confirm; increases blocked and decreases available
- StockAdjustment UNBLOCK creates CR ledger entry on confirm; decreases blocked and increases available
- BLOCK adjustment is rejected if requested quantity exceeds available stock
- UNBLOCK adjustment is rejected if requested quantity exceeds currently blocked stock
- InventoryLedger is immutable -- entries cannot be updated or deleted
- InventoryLedger has no status field and no state machine
- InventoryLedger only has `createdAt` (no `updatedAt`) to enforce immutability
- StockLevel `available` equals `onHand - reserved - blocked`
- Ledger DR/CR balances can be verified against StockLevel at any time
- Atomic commands (receiveStock, issueStock) do not go through a draft-then-confirm lifecycle

## Reference Links

- [SAP Goods Movements (Material Documents)](https://help.sap.com/docs/SAP_S4HANA_ON-PREMISE/f47baaae096c4cf58ad427a8f68493f6/4a28f2e8456c3040e10000000a42189e.html)
- [Odoo Stock Move Model](https://www.odoo.com/documentation/17.0/developer/reference/backend/orm.html)
- [Oracle Inventory Transactions](https://docs.oracle.com/en/cloud/saas/supply-chain-and-manufacturing/25a/fammi/inventory-transactions.html)
