From 21651b396e2c38387c33bb2c5778a20e55905815 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Wed, 26 Nov 2025 07:00:44 +0100 Subject: [PATCH] docs: Add Stock Receipt System documentation to inventory service --- services/inventory/README.md | 356 +++++++++++++++++++++++++++++++++++ 1 file changed, 356 insertions(+) diff --git a/services/inventory/README.md b/services/inventory/README.md index 236ce30c..d664aab9 100644 --- a/services/inventory/README.md +++ b/services/inventory/README.md @@ -624,6 +624,362 @@ async def process_delivery_stock_update(self, event_data: Dict[str, Any]) -> boo } ``` +## Stock Receipt System + +The Stock Receipt System provides lot-level tracking for incoming deliveries with complete food safety compliance and traceability. + +### Overview + +When deliveries arrive, users record receipts through the frontend `StockReceiptModal`, capturing: +- **Lot-Level Details** - Multiple lots per line item (e.g., 100kg delivered in 4×25kg lots) +- **Expiration Dates** - Required for all food ingredients (HACCP compliance) +- **Discrepancy Tracking** - Record when actual ≠ expected quantities +- **Draft Workflow** - Save progress while completing receipt details + +### Database Schema + +**stock_receipts** +```sql +CREATE TABLE stock_receipts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + po_id UUID NOT NULL, -- Links to purchase order + po_number VARCHAR(50) NOT NULL, + + -- Receipt details + received_at TIMESTAMP NOT NULL, + received_by_user_id UUID NOT NULL, + status VARCHAR(50) NOT NULL, -- draft, confirmed, cancelled + + -- Supplier information + supplier_id UUID NOT NULL, + supplier_name VARCHAR(255) NOT NULL, + + -- Metadata + notes TEXT, + has_discrepancies BOOLEAN DEFAULT FALSE, + + -- Timestamps + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + + -- Foreign keys + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (po_id) REFERENCES purchase_orders(id) ON DELETE CASCADE +); + +CREATE INDEX idx_stock_receipts_tenant_po ON stock_receipts(tenant_id, po_id); +CREATE INDEX idx_stock_receipts_status ON stock_receipts(status); +``` + +**stock_receipt_line_items** +```sql +CREATE TABLE stock_receipt_line_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + receipt_id UUID NOT NULL, + + -- Product reference + ingredient_id UUID NOT NULL, + ingredient_name VARCHAR(255) NOT NULL, + po_line_id UUID, -- Links to PO line item + + -- Quantities + expected_quantity DECIMAL(10, 2) NOT NULL, + actual_quantity DECIMAL(10, 2) NOT NULL, + unit_of_measure VARCHAR(50) NOT NULL, + + -- Discrepancy tracking + has_discrepancy BOOLEAN DEFAULT FALSE, + discrepancy_reason TEXT, + + -- Costing + unit_cost DECIMAL(10, 2), + total_cost DECIMAL(10, 2), + + -- Timestamps + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + + -- Foreign keys + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (receipt_id) REFERENCES stock_receipts(id) ON DELETE CASCADE, + FOREIGN KEY (ingredient_id) REFERENCES ingredients(id) ON DELETE CASCADE +); + +CREATE INDEX idx_receipt_line_items_receipt ON stock_receipt_line_items(receipt_id); +``` + +**stock_lots** +```sql +CREATE TABLE stock_lots ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + line_item_id UUID NOT NULL, + stock_id UUID, -- Links to Stock table after confirmation + + -- Lot identification + lot_number VARCHAR(100), -- Internal lot number + supplier_lot_number VARCHAR(100), -- Supplier's lot number + + -- Quantity + quantity DECIMAL(10, 2) NOT NULL, + unit_of_measure VARCHAR(50) NOT NULL, + + -- Food safety (REQUIRED) + expiration_date DATE NOT NULL, -- ⭐ REQUIRED for food safety + best_before_date DATE, + + -- Storage + warehouse_location VARCHAR(255), + storage_zone VARCHAR(100), -- freezer, fridge, dry_storage + + -- Quality + quality_notes TEXT, + + -- Timestamps + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + + -- Foreign keys + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (line_item_id) REFERENCES stock_receipt_line_items(id) ON DELETE CASCADE, + FOREIGN KEY (stock_id) REFERENCES stock(id) ON DELETE SET NULL, + + -- Validation: expiration_date is required + CONSTRAINT check_expiration_date_not_null CHECK (expiration_date IS NOT NULL) +); + +CREATE INDEX idx_stock_lots_line_item ON stock_lots(line_item_id); +CREATE INDEX idx_stock_lots_expiration ON stock_lots(expiration_date); +``` + +### API Endpoints + +**POST /api/v1/inventory/stock-receipts** +Create draft stock receipt +```json +{ + "tenant_id": "uuid", + "po_id": "uuid", + "received_at": "2025-11-26T10:30:00Z", + "received_by_user_id": "uuid", + "line_items": [ + { + "ingredient_id": "uuid", + "expected_quantity": 100.0, + "actual_quantity": 98.0, + "unit_of_measure": "kg", + "has_discrepancy": true, + "discrepancy_reason": "2kg damaged during transport", + "lots": [ + { + "quantity": 25.0, + "expiration_date": "2025-12-26", + "lot_number": "LOT-A-001", + "supplier_lot_number": "SUP-2025-1126" + }, + { + "quantity": 25.0, + "expiration_date": "2025-12-26", + "lot_number": "LOT-A-002" + }, + { + "quantity": 25.0, + "expiration_date": "2025-12-27", + "lot_number": "LOT-B-001" + }, + { + "quantity": 23.0, + "expiration_date": "2025-12-27", + "lot_number": "LOT-B-002" + } + ] + } + ] +} +``` + +**Response**: Created stock receipt with `status=draft` + +**GET /api/v1/inventory/stock-receipts/{receipt_id}** +Retrieve stock receipt details +- Returns full receipt with nested line items and lots +- Used to resume editing draft receipts + +**PUT /api/v1/inventory/stock-receipts/{receipt_id}** +Update draft stock receipt +- Save progress while filling in details +- Validates lot quantity sums +- Replaces line items (cascade delete-and-recreate) + +**POST /api/v1/inventory/stock-receipts/{receipt_id}/confirm** +Finalize stock receipt (atomic transaction) +- Creates Stock records (one per lot) +- Creates StockMovement records (type: PURCHASE) +- Marks receipt status as `confirmed` +- Links lots to stock records via `stock_id` +- **Atomic**: All operations succeed or all fail + +```json +// Request body (minimal) +{ + "confirmed_at": "2025-11-26T10:45:00Z" +} +``` + +**Response**: +```json +{ + "receipt_id": "uuid", + "status": "confirmed", + "stock_records_created": 4, + "total_quantity_added": 98.0, + "stock_ids": ["uuid1", "uuid2", "uuid3", "uuid4"] +} +``` + +### Validation Rules + +**Lot Quantity Validation**: +```python +# Sum of lot quantities must equal actual quantity +sum(lot.quantity for lot in line_item.lots) == line_item.actual_quantity +``` + +**Expiration Date Validation**: +```python +# All food ingredients REQUIRE expiration date +if ingredient.category in ['perishable', 'dairy', 'meat', 'produce']: + if not lot.expiration_date: + raise ValidationError("Expiration date required for food safety") +``` + +**Discrepancy Validation**: +```python +# Discrepancy reason required when actual ≠ expected +if line_item.actual_quantity != line_item.expected_quantity: + line_item.has_discrepancy = True + if not line_item.discrepancy_reason: + raise ValidationError("Discrepancy reason required") +``` + +**Tenant Security**: +```python +# All operations check tenant_id +if receipt.tenant_id != current_user.tenant_id: + raise PermissionError("Access denied") +``` + +### Integration with Alert System + +The stock receipt system integrates tightly with the alert system to guide users through the delivery workflow: + +**DELIVERY_ARRIVING_SOON Alert** (T-2 hours): +- Alert triggers StockReceiptModal in `create` mode +- User creates draft receipt while delivery is in transit +- Pre-fills PO details automatically + +**DELIVERY_OVERDUE Alert** (T+30 minutes): +- Critical priority alert +- Primary action: "Contact Supplier" +- Secondary action: "Mark as Received" (opens StockReceiptModal) + +**STOCK_RECEIPT_INCOMPLETE Alert** (Post-window): +- Important priority alert +- Opens StockReceiptModal in `edit` mode if draft exists +- Shows incomplete line items and missing lot details + +**Workflow**: +``` +1. DELIVERY_ARRIVING_SOON alert appears + ↓ +2. User clicks "Mark as Received" + ↓ +3. StockReceiptModal opens (create mode) + ↓ +4. User enters lot details and expiration dates + ↓ +5. Clicks "Save Draft" (can finish later) + ↓ +6. Later: Returns and clicks "Complete Stock Receipt" + ↓ +7. Reviews, adds missing details + ↓ +8. Clicks "Confirm Receipt" + ↓ +9. System confirms receipt (atomic transaction) + ↓ +10. Triggers delivery.received event + ↓ +11. Auto-resolves all delivery alerts for this PO +``` + +### Food Safety Compliance + +**Why Expiration Dates are Required**: +- HACCP (Hazard Analysis and Critical Control Points) compliance +- Prevents use of expired ingredients +- Enables FIFO consumption based on expiration +- Required for food safety audits in EU/Spain +- Supports lot recall procedures + +**Lot Traceability**: +- Each lot links to supplier lot number +- Complete chain: Supplier → Delivery → Stock → Production → Customer +- Enables rapid recall if contamination detected +- Audit trail for food safety inspectors + +**Storage Zone Tracking**: +- Warehouse location captured per lot +- Storage zone (freezer/fridge/dry) enforces temperature requirements +- Supports temperature monitoring integration + +### Example: Receiving 100kg Flour Delivery + +**Scenario**: PO for 100kg flour arrives in 4×25kg bags + +**Step 1**: Create Draft Receipt +```bash +POST /api/v1/inventory/stock-receipts +{ + "po_id": "PO-12345", + "received_at": "2025-11-26T10:30:00Z", + "line_items": [ + { + "ingredient_id": "flour-00-uuid", + "expected_quantity": 100.0, + "actual_quantity": 100.0, + "unit_of_measure": "kg", + "lots": [ + { + "quantity": 25.0, + "expiration_date": "2026-05-26", + "supplier_lot_number": "MILL-2025-W47-A", + "warehouse_location": "Almacén Principal - Estante 3" + }, + // ... 3 more lots + ] + } + ] +} +``` + +**Step 2**: Confirm Receipt +```bash +POST /api/v1/inventory/stock-receipts/{receipt_id}/confirm +{ + "confirmed_at": "2025-11-26T10:45:00Z" +} +``` + +**Result**: +- 4 Stock records created (one per lot) +- 4 StockMovement records (type: PURCHASE) +- Receipt status: `confirmed` +- Inventory available quantity increases by 100kg +- FIFO will consume oldest expiration date first + ## FIFO Implementation ### FIFO Consumption Logic