docs: Add Stock Receipt System documentation to inventory service
This commit is contained in:
@@ -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 Implementation
|
||||||
|
|
||||||
### FIFO Consumption Logic
|
### FIFO Consumption Logic
|
||||||
|
|||||||
Reference in New Issue
Block a user