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 Consumption Logic
|
||||
|
||||
Reference in New Issue
Block a user