1120 lines
38 KiB
Markdown
1120 lines
38 KiB
Markdown
|
|
# Inventory Service
|
|||
|
|
|
|||
|
|
## Overview
|
|||
|
|
|
|||
|
|
The **Inventory Service** is the operational backbone of Bakery-IA, managing ingredient tracking, stock levels, expiration dates, and food safety compliance. It implements FIFO (First-In-First-Out) consumption logic, automated low-stock alerts, and HACCP-compliant temperature monitoring. This service is critical for achieving zero food waste, maintaining food safety standards, and ensuring bakeries never run out of essential ingredients.
|
|||
|
|
|
|||
|
|
## Key Features
|
|||
|
|
|
|||
|
|
### Comprehensive Ingredient Management
|
|||
|
|
- **Ingredient Catalog** - Complete database of all ingredients with categories
|
|||
|
|
- **Stock Tracking** - Real-time stock levels with FIFO consumption
|
|||
|
|
- **Batch Tracking** - Lot numbers and traceability for food safety
|
|||
|
|
- **Expiration Management** - Automated expiry alerts and FIFO rotation
|
|||
|
|
- **Low Stock Alerts** - Configurable threshold notifications
|
|||
|
|
- **Barcode Support** - Barcode scanning for quick stock updates
|
|||
|
|
- **Multi-Location** - Track inventory across multiple storage locations
|
|||
|
|
|
|||
|
|
### Stock Movement Tracking
|
|||
|
|
- **In/Out Transactions** - Complete audit trail of stock movements
|
|||
|
|
- **Product Transformations** - Track ingredient consumption in production
|
|||
|
|
- **Adjustment Logging** - Record inventory adjustments with reasons
|
|||
|
|
- **Historical Analysis** - Analyze consumption patterns over time
|
|||
|
|
- **Waste Tracking** - Monitor and categorize waste (expired, damaged, etc.)
|
|||
|
|
|
|||
|
|
### Automatic Stock Updates from Deliveries (🆕)
|
|||
|
|
- **Event-Driven Stock Updates** - Automatically updates stock when deliveries are received from procurement
|
|||
|
|
- **Batch & Expiry Tracking** - Records batch/lot numbers and expiration dates from delivery receipts
|
|||
|
|
- **Audit Trail** - Creates complete stock entry records for each delivery
|
|||
|
|
- **Zero Manual Entry** - Eliminates manual stock entry after deliveries
|
|||
|
|
- **Real-Time Synchronization** - Stock levels update immediately when deliveries are recorded
|
|||
|
|
|
|||
|
|
### 🆕 Enterprise Tier: Internal Transfer Processing (NEW)
|
|||
|
|
- **Automatic Ownership Transfer** - When shipments are delivered, inventory ownership automatically transfers from parent to child
|
|||
|
|
- **Stock Deduction at Parent** - Parent's inventory is reduced when shipment departs
|
|||
|
|
- **Stock Addition at Child** - Child's inventory increases when shipment is delivered
|
|||
|
|
- **Transfer Event Processing** - Consumes `shipment.delivered` events from Distribution Service
|
|||
|
|
- **Dual-Sided Recording** - Creates stock movement records for both source (parent) and destination (child)
|
|||
|
|
- **Transfer Movement Type** - Special stock movement type `transfer_out` (parent) and `transfer_in` (child)
|
|||
|
|
- **Audit Trail** - Complete visibility into inter-location transfers
|
|||
|
|
- **Subscription Validation** - Enterprise transfer processing requires Enterprise tier
|
|||
|
|
|
|||
|
|
### Food Safety Compliance (HACCP)
|
|||
|
|
- **Temperature Monitoring** - Critical control point temperature logs
|
|||
|
|
- **Food Safety Alerts** - Automated safety notifications
|
|||
|
|
- **Compliance Tracking** - HACCP compliance audit trail
|
|||
|
|
- **Expiry Management** - Prevent use of expired ingredients
|
|||
|
|
- **Lot Traceability** - Complete ingredient traceability
|
|||
|
|
- **Safety Checklists** - Digital food safety inspection forms
|
|||
|
|
|
|||
|
|
### Sustainability & Reporting
|
|||
|
|
- **Waste Reduction Tracking** - Monitor progress toward zero waste
|
|||
|
|
- **Environmental Impact** - Carbon footprint and sustainability metrics
|
|||
|
|
- **SDG Compliance** - Sustainable Development Goals reporting
|
|||
|
|
- **Grant Reporting** - EU grant compliance reports
|
|||
|
|
- **Business Model Detection** - Auto-detect B2B/B2C inventory patterns
|
|||
|
|
|
|||
|
|
### Dashboard & Analytics
|
|||
|
|
- **Real-Time KPIs** - Current stock levels, expiring items, low stock warnings
|
|||
|
|
- **Consumption Analytics** - Usage patterns and forecasting input
|
|||
|
|
- **Valuation Reports** - Current inventory value and cost tracking
|
|||
|
|
- **Reorder Recommendations** - Intelligent reorder point suggestions
|
|||
|
|
- **Expiry Calendar** - Visual timeline of expiring products
|
|||
|
|
|
|||
|
|
## Business Value
|
|||
|
|
|
|||
|
|
### For Bakery Owners
|
|||
|
|
- **Zero Food Waste Goal** - Reduce waste 20-40% through expiry management and FIFO
|
|||
|
|
- **Food Safety Compliance** - HACCP compliance built-in, avoid health violations
|
|||
|
|
- **Cost Control** - Track inventory value, prevent over-purchasing
|
|||
|
|
- **Never Stock Out** - Automated low-stock alerts ensure continuous operations
|
|||
|
|
- **Traceability** - Complete ingredient tracking for recalls and audits
|
|||
|
|
|
|||
|
|
### Quantifiable Impact
|
|||
|
|
- **Waste Reduction**: 20-40% through FIFO and expiry management
|
|||
|
|
- **Cost Savings**: €200-600/month from reduced waste and better purchasing
|
|||
|
|
- **Time Savings**: 8-12 hours/week on manual inventory tracking
|
|||
|
|
- **Compliance**: 100% HACCP compliance, avoid €5,000+ fines
|
|||
|
|
- **Inventory Accuracy**: 95%+ vs. 70-80% with manual tracking
|
|||
|
|
|
|||
|
|
### For Operations Managers
|
|||
|
|
- **Multi-Location Visibility** - See all inventory across locations
|
|||
|
|
- **Automated Reordering** - System suggests what and when to order
|
|||
|
|
- **Waste Analysis** - Identify patterns and reduce waste
|
|||
|
|
- **Compliance Reporting** - Generate HACCP reports for inspections
|
|||
|
|
|
|||
|
|
## Technology Stack
|
|||
|
|
|
|||
|
|
- **Framework**: FastAPI (Python 3.11+) - Async web framework
|
|||
|
|
- **Database**: PostgreSQL 17 - Ingredient and stock data
|
|||
|
|
- **Caching**: Redis 7.4 - Dashboard KPI cache
|
|||
|
|
- **Messaging**: RabbitMQ 4.1 - Alert publishing
|
|||
|
|
- **ORM**: SQLAlchemy 2.0 (async) - Database abstraction
|
|||
|
|
- **Logging**: Structlog - Structured JSON logging
|
|||
|
|
- **Metrics**: Prometheus Client - Custom metrics
|
|||
|
|
|
|||
|
|
## API Endpoints (Key Routes)
|
|||
|
|
|
|||
|
|
### Ingredient Management
|
|||
|
|
- `POST /api/v1/inventory/ingredients` - Create ingredient
|
|||
|
|
- `GET /api/v1/inventory/ingredients` - List all ingredients
|
|||
|
|
- `GET /api/v1/inventory/ingredients/{ingredient_id}` - Get ingredient details
|
|||
|
|
- `PUT /api/v1/inventory/ingredients/{ingredient_id}` - Update ingredient
|
|||
|
|
- `DELETE /api/v1/inventory/ingredients/{ingredient_id}` - Delete ingredient
|
|||
|
|
|
|||
|
|
### Stock Management
|
|||
|
|
- `GET /api/v1/inventory/stock` - List current stock levels
|
|||
|
|
- `GET /api/v1/inventory/stock/{stock_id}` - Get stock item details
|
|||
|
|
- `POST /api/v1/inventory/stock/adjustment` - Adjust stock levels
|
|||
|
|
- `POST /api/v1/inventory/stock/receive` - Receive new stock
|
|||
|
|
- `POST /api/v1/inventory/stock/consume` - Consume stock (production use)
|
|||
|
|
- `GET /api/v1/inventory/stock/movements` - Stock movement history
|
|||
|
|
|
|||
|
|
### Alerts & Monitoring
|
|||
|
|
- `GET /api/v1/inventory/alerts` - Get active alerts
|
|||
|
|
- `GET /api/v1/inventory/alerts/low-stock` - Low stock items
|
|||
|
|
- `GET /api/v1/inventory/alerts/expiring` - Items expiring soon
|
|||
|
|
- `POST /api/v1/inventory/alerts/configure` - Configure alert thresholds
|
|||
|
|
|
|||
|
|
### Food Safety
|
|||
|
|
- `GET /api/v1/inventory/food-safety/compliance` - HACCP compliance status
|
|||
|
|
- `POST /api/v1/inventory/food-safety/temperature-log` - Log temperature reading
|
|||
|
|
- `GET /api/v1/inventory/food-safety/temperature-logs` - Temperature history
|
|||
|
|
- `POST /api/v1/inventory/food-safety/alert` - Report food safety issue
|
|||
|
|
|
|||
|
|
### Analytics & Reporting
|
|||
|
|
- `GET /api/v1/inventory/dashboard` - Dashboard KPIs
|
|||
|
|
- `GET /api/v1/inventory/analytics/consumption` - Consumption patterns
|
|||
|
|
- `GET /api/v1/inventory/analytics/waste` - Waste analysis
|
|||
|
|
- `GET /api/v1/inventory/analytics/valuation` - Current inventory value
|
|||
|
|
- `GET /api/v1/inventory/reports/haccp` - HACCP compliance report
|
|||
|
|
- `GET /api/v1/inventory/reports/sustainability` - Sustainability report
|
|||
|
|
|
|||
|
|
## Database Schema
|
|||
|
|
|
|||
|
|
### Main Tables
|
|||
|
|
|
|||
|
|
**ingredients**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE ingredients (
|
|||
|
|
id UUID PRIMARY KEY,
|
|||
|
|
tenant_id UUID NOT NULL,
|
|||
|
|
name VARCHAR(255) NOT NULL,
|
|||
|
|
category VARCHAR(100), -- flour, sugar, dairy, etc.
|
|||
|
|
unit VARCHAR(50) NOT NULL, -- kg, liters, units
|
|||
|
|
supplier_id UUID,
|
|||
|
|
reorder_point DECIMAL(10, 2), -- Minimum stock level
|
|||
|
|
reorder_quantity DECIMAL(10, 2), -- Standard order quantity
|
|||
|
|
unit_cost DECIMAL(10, 2),
|
|||
|
|
barcode VARCHAR(100),
|
|||
|
|
storage_location VARCHAR(255),
|
|||
|
|
storage_temperature_min DECIMAL(5, 2),
|
|||
|
|
storage_temperature_max DECIMAL(5, 2),
|
|||
|
|
shelf_life_days INTEGER,
|
|||
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|||
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|||
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|||
|
|
UNIQUE(tenant_id, name)
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**stock**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE stock (
|
|||
|
|
id UUID PRIMARY KEY,
|
|||
|
|
tenant_id UUID NOT NULL,
|
|||
|
|
ingredient_id UUID REFERENCES ingredients(id),
|
|||
|
|
quantity DECIMAL(10, 2) NOT NULL,
|
|||
|
|
unit VARCHAR(50) NOT NULL,
|
|||
|
|
lot_number VARCHAR(100),
|
|||
|
|
received_date DATE NOT NULL,
|
|||
|
|
expiry_date DATE,
|
|||
|
|
supplier_id UUID,
|
|||
|
|
location VARCHAR(255),
|
|||
|
|
unit_cost DECIMAL(10, 2),
|
|||
|
|
status VARCHAR(50) DEFAULT 'available', -- available, reserved, expired, damaged
|
|||
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|||
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|||
|
|
INDEX idx_tenant_ingredient (tenant_id, ingredient_id),
|
|||
|
|
INDEX idx_expiry (expiry_date),
|
|||
|
|
INDEX idx_status (status)
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**stock_movements**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE stock_movements (
|
|||
|
|
id UUID PRIMARY KEY,
|
|||
|
|
tenant_id UUID NOT NULL,
|
|||
|
|
stock_id UUID REFERENCES stock(id),
|
|||
|
|
ingredient_id UUID REFERENCES ingredients(id),
|
|||
|
|
movement_type VARCHAR(50) NOT NULL, -- in, out, adjustment, waste, production, 🆕 transfer_in, transfer_out (NEW)
|
|||
|
|
quantity DECIMAL(10, 2) NOT NULL,
|
|||
|
|
unit VARCHAR(50) NOT NULL,
|
|||
|
|
reference_id UUID, -- production_batch_id, order_id, shipment_id, etc.
|
|||
|
|
reference_type VARCHAR(50), -- production, sale, adjustment, waste, 🆕 internal_transfer (NEW)
|
|||
|
|
reason TEXT,
|
|||
|
|
performed_by UUID,
|
|||
|
|
-- 🆕 Enterprise internal transfer fields (NEW)
|
|||
|
|
source_tenant_id UUID, -- For transfer_out: parent tenant
|
|||
|
|
destination_tenant_id UUID, -- For transfer_in: child tenant
|
|||
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|||
|
|
INDEX idx_tenant_date (tenant_id, created_at),
|
|||
|
|
INDEX idx_ingredient (ingredient_id),
|
|||
|
|
-- 🆕 NEW index for internal transfers
|
|||
|
|
INDEX idx_transfer_tenants (source_tenant_id, destination_tenant_id) WHERE reference_type = 'internal_transfer'
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**stock_alerts**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE stock_alerts (
|
|||
|
|
id UUID PRIMARY KEY,
|
|||
|
|
tenant_id UUID NOT NULL,
|
|||
|
|
ingredient_id UUID REFERENCES ingredients(id),
|
|||
|
|
alert_type VARCHAR(50) NOT NULL, -- low_stock, expiring_soon, expired, temperature
|
|||
|
|
severity VARCHAR(20) NOT NULL, -- low, medium, high, urgent
|
|||
|
|
message TEXT NOT NULL,
|
|||
|
|
current_quantity DECIMAL(10, 2),
|
|||
|
|
threshold_quantity DECIMAL(10, 2),
|
|||
|
|
expiry_date DATE,
|
|||
|
|
days_until_expiry INTEGER,
|
|||
|
|
is_acknowledged BOOLEAN DEFAULT FALSE,
|
|||
|
|
acknowledged_at TIMESTAMP,
|
|||
|
|
acknowledged_by UUID,
|
|||
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|||
|
|
INDEX idx_tenant_active (tenant_id, is_acknowledged)
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**product_transformations**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE product_transformations (
|
|||
|
|
id UUID PRIMARY KEY,
|
|||
|
|
tenant_id UUID NOT NULL,
|
|||
|
|
production_batch_id UUID,
|
|||
|
|
ingredient_id UUID REFERENCES ingredients(id),
|
|||
|
|
quantity_consumed DECIMAL(10, 2) NOT NULL,
|
|||
|
|
unit VARCHAR(50) NOT NULL,
|
|||
|
|
stock_consumed JSONB, -- Array of stock IDs with quantities (FIFO)
|
|||
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**food_safety_compliance**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE food_safety_compliance (
|
|||
|
|
id UUID PRIMARY KEY,
|
|||
|
|
tenant_id UUID NOT NULL,
|
|||
|
|
compliance_type VARCHAR(100), -- haccp, temperature, expiry, cleaning
|
|||
|
|
check_date DATE NOT NULL,
|
|||
|
|
status VARCHAR(50) NOT NULL, -- compliant, non_compliant, warning
|
|||
|
|
details TEXT,
|
|||
|
|
corrective_actions TEXT,
|
|||
|
|
verified_by UUID,
|
|||
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**temperature_logs**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE temperature_logs (
|
|||
|
|
id UUID PRIMARY KEY,
|
|||
|
|
tenant_id UUID NOT NULL,
|
|||
|
|
location VARCHAR(255) NOT NULL, -- freezer_1, fridge_2, storage_room
|
|||
|
|
temperature DECIMAL(5, 2) NOT NULL,
|
|||
|
|
unit VARCHAR(10) DEFAULT 'C',
|
|||
|
|
is_within_range BOOLEAN,
|
|||
|
|
min_acceptable DECIMAL(5, 2),
|
|||
|
|
max_acceptable DECIMAL(5, 2),
|
|||
|
|
recorded_by UUID,
|
|||
|
|
recorded_at TIMESTAMP DEFAULT NOW(),
|
|||
|
|
INDEX idx_tenant_location (tenant_id, location, recorded_at)
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**food_safety_alerts**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE food_safety_alerts (
|
|||
|
|
id UUID PRIMARY KEY,
|
|||
|
|
tenant_id UUID NOT NULL,
|
|||
|
|
alert_type VARCHAR(100), -- temperature_violation, expired_used, contamination
|
|||
|
|
severity VARCHAR(20) NOT NULL,
|
|||
|
|
location VARCHAR(255),
|
|||
|
|
ingredient_id UUID,
|
|||
|
|
description TEXT,
|
|||
|
|
corrective_action_required TEXT,
|
|||
|
|
status VARCHAR(50) DEFAULT 'open', -- open, investigating, resolved
|
|||
|
|
resolved_at TIMESTAMP,
|
|||
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Events & Messaging
|
|||
|
|
|
|||
|
|
### Published Events (RabbitMQ)
|
|||
|
|
|
|||
|
|
**Exchange**: `inventory`
|
|||
|
|
**Routing Keys**: `inventory.low_stock`, `inventory.expiring`, `inventory.food_safety`
|
|||
|
|
|
|||
|
|
**Low Stock Alert Event**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"event_type": "low_stock_alert",
|
|||
|
|
"tenant_id": "uuid",
|
|||
|
|
"ingredient_id": "uuid",
|
|||
|
|
"ingredient_name": "Harina integral",
|
|||
|
|
"current_quantity": 15.5,
|
|||
|
|
"unit": "kg",
|
|||
|
|
"reorder_point": 50.0,
|
|||
|
|
"recommended_order_quantity": 100.0,
|
|||
|
|
"days_until_stockout": 3,
|
|||
|
|
"severity": "high",
|
|||
|
|
"message": "Stock bajo: Harina integral (15.5 kg). Punto de reorden: 50 kg.",
|
|||
|
|
"timestamp": "2025-11-06T10:30:00Z"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Expiring Soon Alert Event**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"event_type": "expiring_soon_alert",
|
|||
|
|
"tenant_id": "uuid",
|
|||
|
|
"stock_id": "uuid",
|
|||
|
|
"ingredient_id": "uuid",
|
|||
|
|
"ingredient_name": "Leche fresca",
|
|||
|
|
"quantity": 20.0,
|
|||
|
|
"unit": "liters",
|
|||
|
|
"lot_number": "LOT-2025-1105",
|
|||
|
|
"expiry_date": "2025-11-09",
|
|||
|
|
"days_until_expiry": 3,
|
|||
|
|
"location": "Nevera 1",
|
|||
|
|
"severity": "medium",
|
|||
|
|
"message": "Leche fresca expira en 3 días (20 litros, LOT-2025-1105)",
|
|||
|
|
"recommended_action": "Usar en producción antes del 09/11/2025",
|
|||
|
|
"timestamp": "2025-11-06T10:30:00Z"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Food Safety Alert Event**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"event_type": "food_safety_alert",
|
|||
|
|
"tenant_id": "uuid",
|
|||
|
|
"alert_type": "temperature_violation",
|
|||
|
|
"severity": "urgent",
|
|||
|
|
"location": "Congelador 2",
|
|||
|
|
"temperature": -12.5,
|
|||
|
|
"acceptable_range": {
|
|||
|
|
"min": -18.0,
|
|||
|
|
"max": -15.0
|
|||
|
|
},
|
|||
|
|
"duration_minutes": 45,
|
|||
|
|
"affected_items": ["uuid1", "uuid2"],
|
|||
|
|
"message": "Violación de temperatura en Congelador 2: -12.5°C (rango: -18°C a -15°C)",
|
|||
|
|
"corrective_action_required": "Revisar congelador inmediatamente y verificar integridad de productos",
|
|||
|
|
"timestamp": "2025-11-06T10:30:00Z"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Consumed Events
|
|||
|
|
|
|||
|
|
**From Procurement Service (🆕)**
|
|||
|
|
- **Delivery Received** (`delivery.received`) - Automatically updates stock levels when deliveries are recorded
|
|||
|
|
- Creates/updates Stock records for each inventory product
|
|||
|
|
- Creates StockEntry audit records with batch/lot numbers and expiry dates
|
|||
|
|
- Handles accepted quantities from delivery receipts
|
|||
|
|
- Links stock movements to delivery reference IDs for full traceability
|
|||
|
|
|
|||
|
|
**🆕 From Distribution Service (NEW)**
|
|||
|
|
- **Shipment Delivered** (`shipment.delivered`) - Automatically processes internal transfers when shipments are delivered
|
|||
|
|
- Decreases stock at parent tenant (creates `transfer_out` stock movement)
|
|||
|
|
- Increases stock at child tenant (creates `transfer_in` stock movement)
|
|||
|
|
- Records source_tenant_id and destination_tenant_id for full transfer traceability
|
|||
|
|
- Links both movements to shipment_id for audit trail
|
|||
|
|
- Enterprise tier validation required
|
|||
|
|
|
|||
|
|
**From Other Services**
|
|||
|
|
- **From Production**: Ingredient consumption in production
|
|||
|
|
- **From Sales**: Finished product sales (for inventory valuation)
|
|||
|
|
|
|||
|
|
## Custom Metrics (Prometheus)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# Stock level metrics
|
|||
|
|
stock_quantity_gauge = Gauge(
|
|||
|
|
'inventory_stock_quantity',
|
|||
|
|
'Current stock quantity',
|
|||
|
|
['tenant_id', 'ingredient_id', 'ingredient_name']
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
low_stock_items_total = Gauge(
|
|||
|
|
'inventory_low_stock_items',
|
|||
|
|
'Number of items below reorder point',
|
|||
|
|
['tenant_id']
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
expiring_items_total = Gauge(
|
|||
|
|
'inventory_expiring_items',
|
|||
|
|
'Number of items expiring within 7 days',
|
|||
|
|
['tenant_id']
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Waste metrics
|
|||
|
|
waste_quantity = Counter(
|
|||
|
|
'inventory_waste_quantity_total',
|
|||
|
|
'Total waste quantity',
|
|||
|
|
['tenant_id', 'ingredient_category', 'reason'] # expired, damaged, etc.
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
waste_value_euros = Counter(
|
|||
|
|
'inventory_waste_value_euros_total',
|
|||
|
|
'Total waste value in euros',
|
|||
|
|
['tenant_id']
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Inventory valuation
|
|||
|
|
inventory_value_total = Gauge(
|
|||
|
|
'inventory_value_euros',
|
|||
|
|
'Total inventory value',
|
|||
|
|
['tenant_id']
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Food safety metrics
|
|||
|
|
temperature_violations = Counter(
|
|||
|
|
'inventory_temperature_violations_total',
|
|||
|
|
'Temperature violations detected',
|
|||
|
|
['tenant_id', 'location']
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
food_safety_alerts_total = Counter(
|
|||
|
|
'inventory_food_safety_alerts_total',
|
|||
|
|
'Food safety alerts generated',
|
|||
|
|
['tenant_id', 'alert_type', 'severity']
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Configuration
|
|||
|
|
|
|||
|
|
### Environment Variables
|
|||
|
|
|
|||
|
|
**Service Configuration:**
|
|||
|
|
- `PORT` - Service port (default: 8005)
|
|||
|
|
- `DATABASE_URL` - PostgreSQL connection string
|
|||
|
|
- `REDIS_URL` - Redis connection string
|
|||
|
|
- `RABBITMQ_URL` - RabbitMQ connection string
|
|||
|
|
|
|||
|
|
**Alert Configuration:**
|
|||
|
|
- `LOW_STOCK_CHECK_INTERVAL_HOURS` - How often to check (default: 6)
|
|||
|
|
- `EXPIRY_WARNING_DAYS` - Days before expiry to alert (default: 7)
|
|||
|
|
- `URGENT_EXPIRY_DAYS` - Days for urgent expiry alerts (default: 3)
|
|||
|
|
- `ENABLE_AUTO_ALERTS` - Automatic alert generation (default: true)
|
|||
|
|
|
|||
|
|
**FIFO Configuration:**
|
|||
|
|
- `ENABLE_FIFO_ENFORCEMENT` - Enforce FIFO consumption (default: true)
|
|||
|
|
- `FIFO_VIOLATION_ALERT` - Alert on FIFO violations (default: true)
|
|||
|
|
|
|||
|
|
**Food Safety Configuration:**
|
|||
|
|
- `TEMPERATURE_CHECK_INTERVAL_MINUTES` - Temp log frequency (default: 60)
|
|||
|
|
- `TEMPERATURE_VIOLATION_THRESHOLD_MINUTES` - Time before alert (default: 30)
|
|||
|
|
- `ENABLE_HACCP_COMPLIANCE` - Enable HACCP tracking (default: true)
|
|||
|
|
|
|||
|
|
**Sustainability Configuration:**
|
|||
|
|
- `TRACK_CARBON_FOOTPRINT` - Enable carbon tracking (default: true)
|
|||
|
|
- `TRACK_SDG_METRICS` - Enable SDG reporting (default: true)
|
|||
|
|
|
|||
|
|
## Development Setup
|
|||
|
|
|
|||
|
|
### Prerequisites
|
|||
|
|
- Python 3.11+
|
|||
|
|
- PostgreSQL 17
|
|||
|
|
- Redis 7.4
|
|||
|
|
- RabbitMQ 4.1 (optional)
|
|||
|
|
|
|||
|
|
### Local Development
|
|||
|
|
```bash
|
|||
|
|
cd services/inventory
|
|||
|
|
python -m venv venv
|
|||
|
|
source venv/bin/activate
|
|||
|
|
|
|||
|
|
pip install -r requirements.txt
|
|||
|
|
|
|||
|
|
export DATABASE_URL=postgresql://user:pass@localhost:5432/inventory
|
|||
|
|
export REDIS_URL=redis://localhost:6379/0
|
|||
|
|
export RABBITMQ_URL=amqp://guest:guest@localhost:5672/
|
|||
|
|
|
|||
|
|
alembic upgrade head
|
|||
|
|
python main.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Testing
|
|||
|
|
```bash
|
|||
|
|
# Unit tests
|
|||
|
|
pytest tests/unit/ -v
|
|||
|
|
|
|||
|
|
# Integration tests
|
|||
|
|
pytest tests/integration/ -v
|
|||
|
|
|
|||
|
|
# FIFO logic tests
|
|||
|
|
pytest tests/test_fifo.py -v
|
|||
|
|
|
|||
|
|
# Test with coverage
|
|||
|
|
pytest --cov=app tests/ --cov-report=html
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Integration Points
|
|||
|
|
|
|||
|
|
### Dependencies
|
|||
|
|
- **Procurement Service** - Receive stock from purchase orders via delivery events (🆕)
|
|||
|
|
- **Production Service** - Consume ingredients in production
|
|||
|
|
- **Forecasting Service** - Provide consumption data for forecasts
|
|||
|
|
- **Suppliers Service** - Supplier information for stock items
|
|||
|
|
- **🆕 Distribution Service** (NEW) - Process internal transfers via shipment.delivered events
|
|||
|
|
- **🆕 Tenant Service** (NEW) - Validate tenant hierarchy for internal transfers
|
|||
|
|
- **PostgreSQL** - Inventory data storage
|
|||
|
|
- **Redis** - Dashboard KPI cache
|
|||
|
|
- **RabbitMQ** - Alert publishing and delivery event consumption (🆕)
|
|||
|
|
|
|||
|
|
### Dependents
|
|||
|
|
- **Production Service** - Check ingredient availability
|
|||
|
|
- **Procurement Service** - Get reorder recommendations
|
|||
|
|
- **AI Insights Service** - Analyze inventory patterns
|
|||
|
|
- **Frontend Dashboard** - Display inventory status
|
|||
|
|
- **Notification Service** - Send inventory alerts
|
|||
|
|
- **🆕 Distribution Service** (NEW) - Verify inventory availability before creating shipments
|
|||
|
|
|
|||
|
|
## Delivery Event Processing (🆕)
|
|||
|
|
|
|||
|
|
### Automatic Stock Updates from Deliveries
|
|||
|
|
|
|||
|
|
The inventory service automatically updates stock levels when deliveries are recorded in the procurement service through event-driven architecture.
|
|||
|
|
|
|||
|
|
**How It Works:**
|
|||
|
|
1. User records delivery receipt in procurement service (frontend `DeliveryReceiptModal`)
|
|||
|
|
2. Procurement service publishes `delivery.received` event to RabbitMQ
|
|||
|
|
3. Inventory service's `DeliveryEventConsumer` listens for and processes the event
|
|||
|
|
4. For each delivered item:
|
|||
|
|
- Gets or creates Stock record for the inventory product
|
|||
|
|
- Updates `quantity_available` by adding accepted quantity
|
|||
|
|
- Creates StockEntry audit record with:
|
|||
|
|
- Transaction type: `RESTOCK`
|
|||
|
|
- Reference type: `DELIVERY`
|
|||
|
|
- Batch/lot number from delivery
|
|||
|
|
- Expiration date from delivery
|
|||
|
|
- Quantity change and new balance
|
|||
|
|
|
|||
|
|
**Benefits:**
|
|||
|
|
- **Zero Manual Entry** - Eliminates duplicate data entry in inventory after recording deliveries
|
|||
|
|
- **Real-Time Updates** - Stock levels update within seconds of delivery recording
|
|||
|
|
- **Complete Traceability** - Every stock change linked to source delivery
|
|||
|
|
- **Batch Tracking** - Automatic batch/lot number recording for food safety compliance
|
|||
|
|
- **Expiry Management** - Expiration dates automatically captured from delivery receipts
|
|||
|
|
|
|||
|
|
### Delivery Event Consumer Implementation
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
async def process_delivery_stock_update(self, event_data: Dict[str, Any]) -> bool:
|
|||
|
|
"""
|
|||
|
|
Process delivery event and update stock levels.
|
|||
|
|
|
|||
|
|
Listens to: procurement.events exchange, routing key: delivery.received
|
|||
|
|
Creates: Stock records and StockEntry audit records
|
|||
|
|
"""
|
|||
|
|
data = event_data.get('data', {})
|
|||
|
|
tenant_id = uuid.UUID(data.get('tenant_id'))
|
|||
|
|
delivery_id = uuid.UUID(data.get('delivery_id'))
|
|||
|
|
items = data.get('items', [])
|
|||
|
|
|
|||
|
|
async with database_manager.get_session() as session:
|
|||
|
|
stock_repo = StockRepository(session)
|
|||
|
|
entry_repo = StockEntryRepository(session)
|
|||
|
|
|
|||
|
|
for item in items:
|
|||
|
|
inventory_product_id = uuid.UUID(item.get('inventory_product_id'))
|
|||
|
|
accepted_quantity = Decimal(str(item.get('accepted_quantity', 0)))
|
|||
|
|
|
|||
|
|
if accepted_quantity <= 0:
|
|||
|
|
continue # Skip rejected items
|
|||
|
|
|
|||
|
|
# Get or create stock record
|
|||
|
|
stock = await stock_repo.get_stock_by_product(
|
|||
|
|
tenant_id=tenant_id,
|
|||
|
|
inventory_product_id=inventory_product_id
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if not stock:
|
|||
|
|
# Create new stock record
|
|||
|
|
stock = await stock_repo.create_stock({
|
|||
|
|
'tenant_id': tenant_id,
|
|||
|
|
'inventory_product_id': inventory_product_id,
|
|||
|
|
'quantity_available': Decimal('0'),
|
|||
|
|
# ... other fields
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# Update stock quantity
|
|||
|
|
new_quantity = stock.quantity_available + accepted_quantity
|
|||
|
|
await stock_repo.update_stock(
|
|||
|
|
stock_id=stock.id,
|
|||
|
|
tenant_id=tenant_id,
|
|||
|
|
update_data={
|
|||
|
|
'quantity_available': new_quantity,
|
|||
|
|
'last_restocked_at': datetime.now(timezone.utc)
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Create stock entry for audit trail
|
|||
|
|
entry = await entry_repo.create_entry({
|
|||
|
|
'tenant_id': tenant_id,
|
|||
|
|
'stock_id': stock.id,
|
|||
|
|
'transaction_type': 'RESTOCK',
|
|||
|
|
'quantity_change': accepted_quantity,
|
|||
|
|
'quantity_after': new_quantity,
|
|||
|
|
'reference_type': 'DELIVERY',
|
|||
|
|
'reference_id': delivery_id,
|
|||
|
|
'batch_number': item.get('batch_lot_number'),
|
|||
|
|
'expiry_date': item.get('expiry_date'),
|
|||
|
|
'notes': f"Delivery received from PO. Batch: {item.get('batch_lot_number', 'N/A')}"
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
await session.commit()
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Event Schema:**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"event_id": "uuid",
|
|||
|
|
"event_type": "delivery.received",
|
|||
|
|
"service_name": "procurement",
|
|||
|
|
"timestamp": "2025-11-12T10:30:00Z",
|
|||
|
|
"data": {
|
|||
|
|
"tenant_id": "uuid",
|
|||
|
|
"delivery_id": "uuid",
|
|||
|
|
"po_id": "uuid",
|
|||
|
|
"received_by": "uuid",
|
|||
|
|
"received_at": "2025-11-12T10:30:00Z",
|
|||
|
|
"items": [
|
|||
|
|
{
|
|||
|
|
"inventory_product_id": "uuid",
|
|||
|
|
"ordered_quantity": 100.0,
|
|||
|
|
"delivered_quantity": 98.0,
|
|||
|
|
"accepted_quantity": 95.0,
|
|||
|
|
"rejected_quantity": 3.0,
|
|||
|
|
"batch_lot_number": "LOT-2025-1112",
|
|||
|
|
"expiry_date": "2025-12-12",
|
|||
|
|
"rejection_reason": "3 damaged units"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 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
|
|||
|
|
```python
|
|||
|
|
async def consume_ingredient_fifo(
|
|||
|
|
tenant_id: str,
|
|||
|
|
ingredient_id: str,
|
|||
|
|
quantity_needed: float
|
|||
|
|
) -> list[dict]:
|
|||
|
|
"""Consume ingredients using FIFO (First-In-First-Out)"""
|
|||
|
|
|
|||
|
|
# Get available stock ordered by received_date (oldest first)
|
|||
|
|
available_stock = await db.query(Stock).filter(
|
|||
|
|
Stock.tenant_id == tenant_id,
|
|||
|
|
Stock.ingredient_id == ingredient_id,
|
|||
|
|
Stock.status == 'available',
|
|||
|
|
Stock.quantity > 0
|
|||
|
|
).order_by(Stock.received_date.asc()).all()
|
|||
|
|
|
|||
|
|
consumed_items = []
|
|||
|
|
remaining_needed = quantity_needed
|
|||
|
|
|
|||
|
|
for stock_item in available_stock:
|
|||
|
|
if remaining_needed <= 0:
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
quantity_to_consume = min(stock_item.quantity, remaining_needed)
|
|||
|
|
|
|||
|
|
# Update stock quantity
|
|||
|
|
stock_item.quantity -= quantity_to_consume
|
|||
|
|
if stock_item.quantity == 0:
|
|||
|
|
stock_item.status = 'depleted'
|
|||
|
|
|
|||
|
|
# Record consumption
|
|||
|
|
consumed_items.append({
|
|||
|
|
'stock_id': stock_item.id,
|
|||
|
|
'lot_number': stock_item.lot_number,
|
|||
|
|
'quantity_consumed': quantity_to_consume,
|
|||
|
|
'received_date': stock_item.received_date,
|
|||
|
|
'expiry_date': stock_item.expiry_date
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
remaining_needed -= quantity_to_consume
|
|||
|
|
|
|||
|
|
if remaining_needed > 0:
|
|||
|
|
raise InsufficientStockError(
|
|||
|
|
f"Insufficient stock. Needed: {quantity_needed}, "
|
|||
|
|
f"Available: {quantity_needed - remaining_needed}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
await db.commit()
|
|||
|
|
return consumed_items
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Security Measures
|
|||
|
|
|
|||
|
|
### Data Protection
|
|||
|
|
- **Tenant Isolation** - All inventory scoped to tenant_id
|
|||
|
|
- **Input Validation** - Validate all quantities and dates
|
|||
|
|
- **Audit Trail** - Complete history of stock movements
|
|||
|
|
- **Access Control** - Role-based permissions
|
|||
|
|
|
|||
|
|
### Food Safety Security
|
|||
|
|
- **Temperature Log Integrity** - Tamper-proof temperature records
|
|||
|
|
- **Lot Traceability** - Complete ingredient tracking for recalls
|
|||
|
|
- **Audit Compliance** - HACCP-compliant record keeping
|
|||
|
|
- **Alert Escalation** - Critical food safety alerts escalate automatically
|
|||
|
|
|
|||
|
|
## Troubleshooting
|
|||
|
|
|
|||
|
|
### Common Issues
|
|||
|
|
|
|||
|
|
**Issue**: FIFO not working correctly
|
|||
|
|
- **Cause**: Stock items missing received_date
|
|||
|
|
- **Solution**: Ensure all stock has received_date set
|
|||
|
|
|
|||
|
|
**Issue**: Low stock alerts not firing
|
|||
|
|
- **Cause**: Reorder points not configured
|
|||
|
|
- **Solution**: Set reorder_point for each ingredient
|
|||
|
|
|
|||
|
|
**Issue**: Expiry alerts too frequent
|
|||
|
|
- **Cause**: `EXPIRY_WARNING_DAYS` set too high
|
|||
|
|
- **Solution**: Adjust to 3-5 days instead of 7
|
|||
|
|
|
|||
|
|
**Issue**: Temperature violations not detected
|
|||
|
|
- **Cause**: Temperature logs not being recorded
|
|||
|
|
- **Solution**: Check temperature monitoring device integration
|
|||
|
|
|
|||
|
|
## Competitive Advantages
|
|||
|
|
|
|||
|
|
1. **FIFO Enforcement** - Automatic expiry prevention
|
|||
|
|
2. **Food Safety Built-In** - HACCP compliance out-of-the-box
|
|||
|
|
3. **Sustainability Tracking** - SDG reporting for EU grants
|
|||
|
|
4. **Barcode Support** - Quick stock updates
|
|||
|
|
5. **Multi-Location** - Track inventory across sites
|
|||
|
|
6. **Spanish Market** - HACCP compliant for Spanish regulations
|
|||
|
|
7. **Zero Waste Focus** - Waste reduction analytics
|
|||
|
|
|
|||
|
|
## Future Enhancements
|
|||
|
|
|
|||
|
|
- **IoT Sensor Integration** - Automatic temperature monitoring
|
|||
|
|
- **AI-Powered Reorder Points** - Dynamic reorder point calculation
|
|||
|
|
- **Image Recognition** - Photo-based stock counting
|
|||
|
|
- **Blockchain Traceability** - Immutable ingredient tracking
|
|||
|
|
- **Mobile Barcode App** - Smartphone barcode scanning
|
|||
|
|
- **Supplier Integration** - Direct supplier ordering
|
|||
|
|
- **Predictive Expiry** - Predict expiry based on storage conditions
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**For VUE Madrid Business Plan**: The Inventory Service demonstrates commitment to food safety (HACCP compliance), sustainability (20-40% waste reduction), and operational excellence. The FIFO enforcement and expiry management features directly address EU food waste regulations and support SDG goals, making this ideal for grant applications. The €200-600/month cost savings and compliance benefits provide clear ROI for bakery owners.
|