# 07 — Inventory & Suppliers

Two stated constraints drive this module:

- **Branches are independent** → stock, costs and counts are strictly **per branch**.
- **Suppliers may be shared** → supplier master data is **global**, purchasing is per branch.

```
Supplier (global)
    │
    └──▶ PurchaseOrder (branch)  ──▶ GoodsReceipt (branch)  ──▶ StockMovement (+)
                                          │
                                          └──▶ SupplierInvoice ──▶ SupplierPayment  (doc 08)

Sale (branch) ──▶ recipe explosion ──▶ StockMovement (−)
Waste / Transfer / Count ─────────────▶ StockMovement (±)
```

---

## 1. Inventory item

Distinct from a **menu item** (doc 02). A menu item is what the customer buys; an inventory
item is what the branch stores.

| Field | Notes |
|---|---|
| `sku`, `name_ar`, `name_en` | |
| `category` | Coffee, Dairy, Syrups, Bakery, Packaging, Cleaning, Gas, Other |
| `base_unit` | The unit stock is held in: `g`, `ml`, `piece` |
| `purchase_units[]` | e.g. `bag = 1000 g`, `case = 24 piece`. Conversion factor required. |
| `is_perishable`, `shelf_life_days` | Drives expiry alerts |
| `is_sold_directly` | True for retail items that are also menu items (bottled water) |
| `default_supplier_id` | |
| `is_active` | |

**Rule:** stock is always held and reported in `base_unit`. Purchase units convert on
receipt. Mixing units in the ledger is the fastest way to make stock numbers meaningless.

---

## 2. Per-branch stock

| Field | Notes |
|---|---|
| `branch_id`, `inventory_item_id` | Composite key |
| `quantity_on_hand` | Derived from the movement ledger, never directly written |
| `reorder_point`, `reorder_quantity` | Per branch — Basrah's velocity is not Karbala's |
| `average_cost` | Weighted average, **per branch** |
| `last_counted_at`, `last_received_at` | |
| `storage_location` | Optional: main store / bar / fridge |

**Costing method: weighted average per branch.** FIFO is more precise but demands lot
tracking on every movement, which staff will not maintain reliably. Weighted average is
accurate enough for management accounts and survives imperfect data entry.

---

## 3. Stock movement ledger

Everything is a movement. `quantity_on_hand` is the sum of movements — a derived value, so
it can always be reconstructed and can never silently disagree with its own history.

| Type | Sign | Source |
|---|---|---|
| `receipt` | + | Goods received from a supplier |
| `sale` | − | Recipe explosion at order confirmation |
| `void_return` | + | Reversal of a voided order |
| `transfer_out` / `transfer_in` | − / + | Between branches |
| `waste` | − | Spoilage, breakage, expiry, staff error |
| `staff_consumption` | − | Staff drinks/meals |
| `sample_marketing` | − | Free tastings, goodwill |
| `count_adjustment` | ± | Stock count reconciliation |
| `manual_adjustment` | ± | Correction — always requires reason + approval |

Each movement records `quantity`, `unit_cost`, `branch_id`, `user_id`,
`approved_by_user_id?`, `reason_code`, `reference` (order / PO / transfer / count id),
`occurred_at`, `business_day`.

**Every negative adjustment needs a reason code.** "Adjust to match" with no reason is how
theft hides.

---

## 4. Suppliers

Global master data, shared across branches.

| Field | Notes |
|---|---|
| `name_ar`, `name_en`, `code` | |
| `contacts[]` | Name, phone, WhatsApp — how ordering actually happens in practice |
| `supplied_branches[]` | Which branches this supplier serves |
| `payment_terms` | `cash_on_delivery` / `net_days(n)` / `end_of_month` |
| `currency` | IQD or USD (imported goods are often quoted in USD) |
| `lead_time_days` | Feeds reorder suggestions |
| `delivery_days[]` | e.g. Sun/Wed — a supplier who delivers twice a week changes reorder timing |
| `rating`, `notes` | |
| `is_active` | |

### Supplier price list

```
supplier_price { supplier_id, inventory_item_id, purchase_unit, price, currency,
                 valid_from, valid_to, min_order_qty }
```

Because branches buy the same items from possibly the same suppliers, this enables the
**Price Variance by Branch** report — "Basrah pays 12% more for the same milk from the
same supplier". At five branches this is one of the highest-value reports in the system and
it costs almost nothing once supplier prices are recorded.

---

## 5. Purchasing

```
Draft PO ──▶ Approved ──▶ Sent ──▶ Partially received ──▶ Received ──▶ Invoiced ──▶ Paid
     │                                                                    │
     └──▶ Cancelled                                                       └──▶ (doc 08)
```

### Purchase order

| Field | Notes |
|---|---|
| `po_number` | Per branch, sequential |
| `branch_id`, `supplier_id` | |
| `lines[]` | `item, purchase_unit, quantity, unit_price, line_total` |
| `expected_date`, `currency`, `fx_rate?` | |
| `created_by`, `approved_by`, `status` | Approval threshold configurable per branch |
| `notes` | |

**Reorder suggestions.** The system proposes a PO when `quantity_on_hand ≤ reorder_point`,
sized from consumption over the trailing N days and the supplier's `lead_time_days` and
`delivery_days`. It is a **suggestion requiring confirmation**, never an automatic order.

### Goods receipt (GRN)

| Rule |
|---|
| Records **actually received** quantities, which may differ from ordered |
| Short/over deliveries are recorded as-is and reported; the PO is not silently amended |
| Generates positive stock movements at the **received unit cost**, which updates weighted average cost |
| Damaged goods are received then immediately wasted with reason `damaged_on_delivery`, so the supplier claim is evidenced |
| Perishables capture `expiry_date` at receipt |
| Receipt requires `inventory.receive`; the receiver is recorded |

**Price change alert:** if the received unit cost differs from the last purchase by more
than a configurable threshold, flag it. Silent supplier price creep is a real margin leak
and nobody notices it at the counter.

---

## 6. Inter-branch transfers

Branches are independent, but Baghdad Main will lend Karbala coffee beans in an emergency.
Transfers must be **two-sided**, because one-sided transfers are where stock disappears.

```
Requested ──▶ Approved ──▶ Sent ──▶ Received ──▶ Completed
                              └──▶ Discrepancy ──▶ Resolved
```

| Rule |
|---|
| Sending branch: `transfer_out` movement on dispatch |
| Receiving branch: `transfer_in` movement only on **confirmed receipt** |
| Stock in transit is visible and belongs to neither branch's on-hand |
| A receipt quantity differing from the sent quantity opens a **discrepancy** requiring manager resolution |
| Transfer cost = the sending branch's weighted average cost, so neither branch's margin is distorted |
| Transfers between distant cities (Baghdad ↔ Basrah) should be rare; frequency is reported |

---

## 7. Waste, staff consumption and samples

Separate categories, because they mean different things and different people are
accountable.

| Category | Reason codes |
|---|---|
| **Waste** | `expired`, `spoiled`, `broken`, `spilled`, `prep_error`, `customer_return`, `damaged_on_delivery` |
| **Staff consumption** | Linked to the employee; optionally deducted at payroll (doc 08) |
| **Marketing / sample** | Linked to a campaign or approved by the manager |

Recording is a **fast one-tap flow from the bar screen** — if it takes thirty seconds,
staff will not do it and the number becomes fiction. Wasted prepared drinks deduct the
recipe's ingredients, not a vague "one latte".

Daily waste value per branch, and waste as a percentage of sales, are standing KPIs.

---

## 8. Stock counts

| Type | Frequency | Scope |
|---|---|---|
| **Spot count** | Daily | High-value / high-theft items: coffee, milk, cigarettes, cash-like stock |
| **Cycle count** | Weekly | One category rotating through the month |
| **Full count** | Monthly | Everything; the basis for accurate COGS |

### Process

```
1. Open a count sheet for a branch + scope        → snapshot expected quantities
2. Counters enter actual quantities (blind: expected hidden by default)
3. System computes variance in quantity and value
4. Variances beyond tolerance require a reason and manager approval
5. Approve the count → count_adjustment movements are posted
6. Count is frozen and archived
```

**Rules**
- Blind counting is the default, for the same reason as blind cash close (doc 06 §3).
- The count period must be closed to other movements while counting, or the variance is
  noise. Practically: count before opening or after closing.
- A count cannot be edited after approval; a mistake requires a new count.

### Theoretical vs actual — the point of the whole module

```
Theoretical usage  = Σ recipe quantities of everything sold
Actual usage       = opening stock + receipts + transfers_in − transfers_out − closing stock
Shrinkage          = Actual − Theoretical − recorded waste
```

Persistent unexplained shrinkage on a specific item at a specific branch is the single most
useful loss signal this system produces. It is meaningless without recipes — which is why
recipes (doc 02 §6) matter more than they first appear.

---

## 9. Alerts

| Alert | To whom |
|---|---|
| Below reorder point | Branch Manager, Storekeeper |
| Zero stock on an item with an active recipe | Branch Manager (POS auto-86 warning) |
| Expiring within N days | Storekeeper |
| Purchase price increased beyond threshold | Branch Manager, Owner |
| Stock count variance beyond threshold | Branch Manager, Owner |
| Negative stock on hand | Branch Manager — always a data error worth chasing |
| Transfer not received within N days | Both branch managers |
