36 KiB
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
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 ingredientGET /api/v1/inventory/ingredients- List all ingredientsGET /api/v1/inventory/ingredients/{ingredient_id}- Get ingredient detailsPUT /api/v1/inventory/ingredients/{ingredient_id}- Update ingredientDELETE /api/v1/inventory/ingredients/{ingredient_id}- Delete ingredient
Stock Management
GET /api/v1/inventory/stock- List current stock levelsGET /api/v1/inventory/stock/{stock_id}- Get stock item detailsPOST /api/v1/inventory/stock/adjustment- Adjust stock levelsPOST /api/v1/inventory/stock/receive- Receive new stockPOST /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 alertsGET /api/v1/inventory/alerts/low-stock- Low stock itemsGET /api/v1/inventory/alerts/expiring- Items expiring soonPOST /api/v1/inventory/alerts/configure- Configure alert thresholds
Food Safety
GET /api/v1/inventory/food-safety/compliance- HACCP compliance statusPOST /api/v1/inventory/food-safety/temperature-log- Log temperature readingGET /api/v1/inventory/food-safety/temperature-logs- Temperature historyPOST /api/v1/inventory/food-safety/alert- Report food safety issue
Analytics & Reporting
GET /api/v1/inventory/dashboard- Dashboard KPIsGET /api/v1/inventory/analytics/consumption- Consumption patternsGET /api/v1/inventory/analytics/waste- Waste analysisGET /api/v1/inventory/analytics/valuation- Current inventory valueGET /api/v1/inventory/reports/haccp- HACCP compliance reportGET /api/v1/inventory/reports/sustainability- Sustainability report
Database Schema
Main Tables
ingredients
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
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
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
quantity DECIMAL(10, 2) NOT NULL,
unit VARCHAR(50) NOT NULL,
reference_id UUID, -- production_batch_id, order_id, etc.
reference_type VARCHAR(50), -- production, sale, adjustment, waste
reason TEXT,
performed_by UUID,
created_at TIMESTAMP DEFAULT NOW(),
INDEX idx_tenant_date (tenant_id, created_at),
INDEX idx_ingredient (ingredient_id)
);
stock_alerts
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
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
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
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
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
{
"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
{
"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
{
"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 Other Services
- From Production: Ingredient consumption in production
- From Sales: Finished product sales (for inventory valuation)
Custom Metrics (Prometheus)
# 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 stringREDIS_URL- Redis connection stringRABBITMQ_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
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
# 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
- 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
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:
- User records delivery receipt in procurement service (frontend
DeliveryReceiptModal) - Procurement service publishes
delivery.receivedevent to RabbitMQ - Inventory service's
DeliveryEventConsumerlistens for and processes the event - For each delivered item:
- Gets or creates Stock record for the inventory product
- Updates
quantity_availableby 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
- Transaction type:
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
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:
{
"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
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
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
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
{
"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
// Request body (minimal)
{
"confirmed_at": "2025-11-26T10:45:00Z"
}
Response:
{
"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:
# Sum of lot quantities must equal actual quantity
sum(lot.quantity for lot in line_item.lots) == line_item.actual_quantity
Expiration Date Validation:
# 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:
# 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:
# 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
createmode - 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
editmode 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
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
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
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_DAYSset 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
- FIFO Enforcement - Automatic expiry prevention
- Food Safety Built-In - HACCP compliance out-of-the-box
- Sustainability Tracking - SDG reporting for EU grants
- Barcode Support - Quick stock updates
- Multi-Location - Track inventory across sites
- Spanish Market - HACCP compliant for Spanish regulations
- 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.