Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

View File

@@ -0,0 +1,250 @@
# ================================================================
# services/inventory/app/schemas/dashboard.py
# ================================================================
"""
Dashboard and analytics schemas for Inventory Service
"""
from datetime import datetime
from decimal import Decimal
from typing import List, Optional, Dict, Any
from uuid import UUID
from pydantic import BaseModel, Field
# ===== Dashboard Summary Schemas =====
class InventoryDashboardSummary(BaseModel):
"""Comprehensive dashboard summary for inventory management"""
# Current inventory metrics
total_ingredients: int
active_ingredients: int
total_stock_value: Decimal
total_stock_items: int
# Stock status breakdown
in_stock_items: int
low_stock_items: int
out_of_stock_items: int
expired_items: int
expiring_soon_items: int
# Food safety metrics
food_safety_alerts_active: int
temperature_violations_today: int
compliance_issues: int
certifications_expiring_soon: int
# Recent activity
recent_stock_movements: int
recent_purchases: int
recent_waste: int
recent_adjustments: int
# Business model context
business_model: Optional[str] = None # individual_bakery, central_bakery
business_model_confidence: Optional[Decimal] = None
# Category breakdown
stock_by_category: Dict[str, Any]
alerts_by_severity: Dict[str, int]
movements_by_type: Dict[str, int]
# Performance indicators
inventory_turnover_ratio: Optional[Decimal] = None
waste_percentage: Optional[Decimal] = None
compliance_score: Optional[Decimal] = None
cost_per_unit_avg: Optional[Decimal] = None
# Trending data
stock_value_trend: List[Dict[str, Any]] = []
alert_trend: List[Dict[str, Any]] = []
class Config:
from_attributes = True
class StockStatusSummary(BaseModel):
"""Summary of stock status by category"""
category: str
total_ingredients: int
in_stock: int
low_stock: int
out_of_stock: int
total_value: Decimal
percentage_of_total: Decimal
class AlertSummary(BaseModel):
"""Summary of alerts by type and severity"""
alert_type: str
severity: str
count: int
oldest_alert_age_hours: Optional[int] = None
average_resolution_time_hours: Optional[int] = None
class RecentActivity(BaseModel):
"""Recent activity item for dashboard"""
activity_type: str # stock_added, stock_consumed, alert_created, etc.
description: str
timestamp: datetime
user_name: Optional[str] = None
impact_level: str = Field(default="low") # low, medium, high
entity_id: Optional[UUID] = None
entity_type: Optional[str] = None
# ===== Food Safety Dashboard Schemas =====
class FoodSafetyDashboard(BaseModel):
"""Food safety specific dashboard metrics"""
# Compliance overview
total_compliance_items: int
compliant_items: int
non_compliant_items: int
pending_review_items: int
compliance_percentage: Decimal
# Temperature monitoring
temperature_sensors_online: int
temperature_sensors_total: int
temperature_violations_24h: int
current_temperature_status: str # all_good, warnings, violations
# Expiration tracking
items_expiring_today: int
items_expiring_this_week: int
expired_items_requiring_action: int
# Audit and certification status
upcoming_audits: int
overdue_audits: int
certifications_valid: int
certifications_expiring_soon: int
# Risk assessment
high_risk_items: int
critical_alerts: int
regulatory_notifications_pending: int
# Recent safety events
recent_safety_incidents: List[RecentActivity] = []
class Config:
from_attributes = True
class TemperatureMonitoringStatus(BaseModel):
"""Current temperature monitoring status"""
location: str
equipment_id: Optional[str] = None
current_temperature: Decimal
target_min: Decimal
target_max: Decimal
status: str # normal, warning, critical
last_reading: datetime
hours_since_last_reading: Decimal
alert_active: bool = False
class ComplianceStatusSummary(BaseModel):
"""Compliance status summary by standard"""
standard: str
standard_name: str
total_items: int
compliant: int
non_compliant: int
pending_review: int
expired: int
compliance_rate: Decimal
next_audit_date: Optional[datetime] = None
# ===== Analytics and Reporting Schemas =====
class InventoryAnalytics(BaseModel):
"""Advanced analytics for inventory management"""
# Turnover analysis
inventory_turnover_rate: Decimal
fast_moving_items: List[Dict[str, Any]]
slow_moving_items: List[Dict[str, Any]]
dead_stock_items: List[Dict[str, Any]]
# Cost analysis
total_inventory_cost: Decimal
cost_by_category: Dict[str, Decimal]
average_unit_cost_trend: List[Dict[str, Any]]
waste_cost_analysis: Dict[str, Any]
# Efficiency metrics
stockout_frequency: Dict[str, int]
overstock_frequency: Dict[str, int]
reorder_accuracy: Decimal
forecast_accuracy: Decimal
# Quality and safety metrics
quality_incidents_rate: Decimal
food_safety_score: Decimal
compliance_score_by_standard: Dict[str, Decimal]
temperature_compliance_rate: Decimal
# Supplier performance
supplier_performance: List[Dict[str, Any]]
delivery_reliability: Decimal
quality_consistency: Decimal
class Config:
from_attributes = True
class BusinessModelInsights(BaseModel):
"""Business model insights based on inventory patterns"""
detected_model: str # individual_bakery, central_bakery, mixed
confidence_score: Decimal
# Model characteristics
total_ingredient_types: int
average_stock_per_ingredient: Decimal
finished_product_ratio: Decimal
supplier_diversity: int
# Operational patterns
order_frequency_pattern: str
seasonal_variation: bool
bulk_purchasing_indicator: Decimal
production_scale_indicator: str
# Recommendations
business_model_specific_recommendations: List[str]
optimization_opportunities: List[str]
class Config:
from_attributes = True
# ===== Request/Filter Schemas =====
class DashboardFilter(BaseModel):
"""Filtering options for dashboard data"""
date_from: Optional[datetime] = None
date_to: Optional[datetime] = None
categories: Optional[List[str]] = None
severity_levels: Optional[List[str]] = None
alert_types: Optional[List[str]] = None
business_model: Optional[str] = None
include_inactive: bool = False
class AlertsFilter(BaseModel):
"""Filtering options for alerts dashboard"""
alert_types: Optional[List[str]] = None
severities: Optional[List[str]] = None
statuses: Optional[List[str]] = None
date_from: Optional[datetime] = None
date_to: Optional[datetime] = None
assigned_to: Optional[UUID] = None
unresolved_only: bool = True

View File

@@ -0,0 +1,283 @@
# ================================================================
# services/inventory/app/schemas/food_safety.py
# ================================================================
"""
Food safety schemas for Inventory Service
"""
from datetime import datetime
from decimal import Decimal
from typing import List, Optional, Dict, Any
from uuid import UUID
from pydantic import BaseModel, Field, validator
# ===== Food Safety Compliance Schemas =====
class FoodSafetyComplianceBase(BaseModel):
ingredient_id: UUID
standard: str
compliance_status: str = Field(default="pending_review")
certification_number: Optional[str] = None
certifying_body: Optional[str] = None
certification_date: Optional[datetime] = None
expiration_date: Optional[datetime] = None
requirements: Optional[Dict[str, Any]] = None
compliance_notes: Optional[str] = None
documentation_url: Optional[str] = None
last_audit_date: Optional[datetime] = None
next_audit_date: Optional[datetime] = None
auditor_name: Optional[str] = None
audit_score: Optional[float] = Field(None, ge=0, le=100)
risk_level: str = Field(default="medium")
risk_factors: Optional[List[str]] = None
mitigation_measures: Optional[List[str]] = None
requires_monitoring: bool = Field(default=True)
monitoring_frequency_days: Optional[int] = Field(None, gt=0)
class FoodSafetyComplianceCreate(FoodSafetyComplianceBase):
tenant_id: UUID
class FoodSafetyComplianceUpdate(BaseModel):
compliance_status: Optional[str] = None
certification_number: Optional[str] = None
certifying_body: Optional[str] = None
certification_date: Optional[datetime] = None
expiration_date: Optional[datetime] = None
requirements: Optional[Dict[str, Any]] = None
compliance_notes: Optional[str] = None
documentation_url: Optional[str] = None
last_audit_date: Optional[datetime] = None
next_audit_date: Optional[datetime] = None
auditor_name: Optional[str] = None
audit_score: Optional[float] = Field(None, ge=0, le=100)
risk_level: Optional[str] = None
risk_factors: Optional[List[str]] = None
mitigation_measures: Optional[List[str]] = None
requires_monitoring: Optional[bool] = None
monitoring_frequency_days: Optional[int] = Field(None, gt=0)
class FoodSafetyComplianceResponse(FoodSafetyComplianceBase):
id: UUID
tenant_id: UUID
is_active: bool
created_at: datetime
updated_at: datetime
created_by: Optional[UUID] = None
updated_by: Optional[UUID] = None
class Config:
from_attributes = True
# ===== Temperature Monitoring Schemas =====
class TemperatureLogBase(BaseModel):
storage_location: str = Field(..., min_length=1, max_length=100)
warehouse_zone: Optional[str] = Field(None, max_length=50)
equipment_id: Optional[str] = Field(None, max_length=100)
temperature_celsius: float
humidity_percentage: Optional[float] = Field(None, ge=0, le=100)
target_temperature_min: Optional[float] = None
target_temperature_max: Optional[float] = None
measurement_method: str = Field(default="manual")
device_id: Optional[str] = Field(None, max_length=100)
calibration_date: Optional[datetime] = None
class TemperatureLogCreate(TemperatureLogBase):
tenant_id: UUID
class TemperatureLogResponse(TemperatureLogBase):
id: UUID
tenant_id: UUID
is_within_range: bool
alert_triggered: bool
deviation_minutes: Optional[int] = None
recorded_at: datetime
created_at: datetime
recorded_by: Optional[UUID] = None
class Config:
from_attributes = True
# ===== Food Safety Alert Schemas =====
class FoodSafetyAlertBase(BaseModel):
alert_type: str
severity: str = Field(default="medium")
risk_level: str = Field(default="medium")
source_entity_type: str
source_entity_id: UUID
ingredient_id: Optional[UUID] = None
stock_id: Optional[UUID] = None
title: str = Field(..., min_length=1, max_length=200)
description: str = Field(..., min_length=1)
detailed_message: Optional[str] = None
regulatory_requirement: Optional[str] = Field(None, max_length=100)
compliance_standard: Optional[str] = None
regulatory_action_required: bool = Field(default=False)
trigger_condition: Optional[str] = Field(None, max_length=200)
threshold_value: Optional[Decimal] = None
actual_value: Optional[Decimal] = None
alert_data: Optional[Dict[str, Any]] = None
environmental_factors: Optional[Dict[str, Any]] = None
affected_products: Optional[List[UUID]] = None
public_health_risk: bool = Field(default=False)
business_impact: Optional[str] = None
estimated_loss: Optional[Decimal] = Field(None, ge=0)
class FoodSafetyAlertCreate(FoodSafetyAlertBase):
tenant_id: UUID
alert_code: str = Field(..., min_length=1, max_length=50)
class FoodSafetyAlertUpdate(BaseModel):
status: Optional[str] = None
alert_state: Optional[str] = None
immediate_actions_taken: Optional[List[str]] = None
investigation_notes: Optional[str] = None
resolution_action: Optional[str] = Field(None, max_length=200)
resolution_notes: Optional[str] = None
corrective_actions: Optional[List[str]] = None
preventive_measures: Optional[List[str]] = None
assigned_to: Optional[UUID] = None
assigned_role: Optional[str] = Field(None, max_length=50)
escalated_to: Optional[UUID] = None
escalation_deadline: Optional[datetime] = None
documentation: Optional[Dict[str, Any]] = None
class FoodSafetyAlertResponse(FoodSafetyAlertBase):
id: UUID
tenant_id: UUID
alert_code: str
status: str
alert_state: str
immediate_actions_taken: Optional[List[str]] = None
investigation_notes: Optional[str] = None
resolution_action: Optional[str] = None
resolution_notes: Optional[str] = None
corrective_actions: Optional[List[str]] = None
preventive_measures: Optional[List[str]] = None
first_occurred_at: datetime
last_occurred_at: datetime
acknowledged_at: Optional[datetime] = None
resolved_at: Optional[datetime] = None
escalation_deadline: Optional[datetime] = None
occurrence_count: int
is_recurring: bool
recurrence_pattern: Optional[str] = None
assigned_to: Optional[UUID] = None
assigned_role: Optional[str] = None
escalated_to: Optional[UUID] = None
escalation_level: int
notification_sent: bool
notification_methods: Optional[List[str]] = None
notification_recipients: Optional[List[str]] = None
regulatory_notification_required: bool
regulatory_notification_sent: bool
documentation: Optional[Dict[str, Any]] = None
audit_trail: Optional[List[Dict[str, Any]]] = None
external_reference: Optional[str] = None
detection_time: Optional[datetime] = None
response_time_minutes: Optional[int] = None
resolution_time_minutes: Optional[int] = None
alert_accuracy: Optional[bool] = None
false_positive: bool
feedback_notes: Optional[str] = None
created_at: datetime
updated_at: datetime
created_by: Optional[UUID] = None
updated_by: Optional[UUID] = None
class Config:
from_attributes = True
# ===== Bulk Operations Schemas =====
class BulkTemperatureLogCreate(BaseModel):
"""Schema for bulk temperature logging"""
tenant_id: UUID
readings: List[TemperatureLogBase] = Field(..., min_items=1, max_items=100)
class BulkComplianceUpdate(BaseModel):
"""Schema for bulk compliance updates"""
tenant_id: UUID
updates: List[Dict[str, Any]] = Field(..., min_items=1, max_items=50)
# ===== Filter and Query Schemas =====
class FoodSafetyFilter(BaseModel):
"""Filtering options for food safety data"""
compliance_standards: Optional[List[str]] = None
compliance_statuses: Optional[List[str]] = None
risk_levels: Optional[List[str]] = None
alert_types: Optional[List[str]] = None
severities: Optional[List[str]] = None
date_from: Optional[datetime] = None
date_to: Optional[datetime] = None
assigned_to: Optional[UUID] = None
include_resolved: bool = False
regulatory_action_required: Optional[bool] = None
class TemperatureMonitoringFilter(BaseModel):
"""Filtering options for temperature monitoring"""
storage_locations: Optional[List[str]] = None
equipment_ids: Optional[List[str]] = None
date_from: Optional[datetime] = None
date_to: Optional[datetime] = None
violations_only: bool = False
alerts_only: bool = False
# ===== Analytics Schemas =====
class FoodSafetyMetrics(BaseModel):
"""Food safety performance metrics"""
compliance_rate: Decimal = Field(..., ge=0, le=100)
temperature_compliance_rate: Decimal = Field(..., ge=0, le=100)
alert_response_time_avg: Optional[Decimal] = None
alert_resolution_time_avg: Optional[Decimal] = None
recurring_issues_count: int
regulatory_violations: int
certification_coverage: Decimal = Field(..., ge=0, le=100)
audit_score_avg: Optional[Decimal] = Field(None, ge=0, le=100)
risk_score: Decimal = Field(..., ge=0, le=10)
class TemperatureAnalytics(BaseModel):
"""Temperature monitoring analytics"""
total_readings: int
violations_count: int
violation_rate: Decimal = Field(..., ge=0, le=100)
average_temperature: Decimal
temperature_range: Dict[str, Decimal]
longest_violation_hours: Optional[int] = None
equipment_performance: List[Dict[str, Any]]
location_performance: List[Dict[str, Any]]
# ===== Notification Schemas =====
class AlertNotificationPreferences(BaseModel):
"""User preferences for alert notifications"""
email_enabled: bool = True
sms_enabled: bool = False
whatsapp_enabled: bool = False
dashboard_enabled: bool = True
severity_threshold: str = Field(default="medium") # Only notify for this severity and above
alert_types: Optional[List[str]] = None # Specific alert types to receive
quiet_hours_start: Optional[str] = Field(None, pattern=r"^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$")
quiet_hours_end: Optional[str] = Field(None, pattern=r"^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$")
weekend_notifications: bool = True

View File

@@ -0,0 +1,631 @@
# services/inventory/app/schemas/inventory.py
"""
Pydantic schemas for inventory API requests and responses
"""
from typing import Optional, List, Dict, Any
from datetime import datetime
from decimal import Decimal
from uuid import UUID
from pydantic import BaseModel, Field, validator
from typing import Generic, TypeVar
from enum import Enum
from app.models.inventory import UnitOfMeasure, IngredientCategory, StockMovementType, ProductType, ProductCategory, ProductionStage
T = TypeVar('T')
# ===== BASE SCHEMAS =====
class InventoryBaseSchema(BaseModel):
"""Base schema for inventory models"""
class Config:
from_attributes = True
use_enum_values = True
json_encoders = {
datetime: lambda v: v.isoformat() if v else None,
Decimal: lambda v: float(v) if v else None
}
# ===== INGREDIENT SCHEMAS =====
class IngredientCreate(InventoryBaseSchema):
"""Schema for creating ingredients and finished products"""
name: str = Field(..., max_length=255, description="Product name")
product_type: ProductType = Field(ProductType.INGREDIENT, description="Type of product (ingredient or finished_product)")
sku: Optional[str] = Field(None, max_length=100, description="SKU code")
barcode: Optional[str] = Field(None, max_length=50, description="Barcode")
category: Optional[str] = Field(None, description="Product category (ingredient or finished product category)")
subcategory: Optional[str] = Field(None, max_length=100, description="Subcategory")
description: Optional[str] = Field(None, description="Ingredient description")
brand: Optional[str] = Field(None, max_length=100, description="Brand name")
unit_of_measure: UnitOfMeasure = Field(..., description="Unit of measure")
package_size: Optional[float] = Field(None, gt=0, description="Package size")
# Pricing
# Note: average_cost is calculated automatically from purchases (not set on create)
# All cost fields are optional - can be added later after onboarding
standard_cost: Optional[Decimal] = Field(None, ge=0, description="Standard/target cost per unit for budgeting")
# Stock management - all optional with sensible defaults for onboarding
# These can be configured later based on actual usage patterns
low_stock_threshold: Optional[float] = Field(None, ge=0, description="Low stock alert threshold")
reorder_point: Optional[float] = Field(None, ge=0, description="Reorder point")
reorder_quantity: Optional[float] = Field(None, gt=0, description="Default reorder quantity")
max_stock_level: Optional[float] = Field(None, gt=0, description="Maximum stock level")
# Shelf life (default value only - actual per batch)
shelf_life_days: Optional[int] = Field(None, gt=0, description="Default shelf life in days")
# Properties
is_perishable: bool = Field(False, description="Is perishable")
allergen_info: Optional[Dict[str, Any]] = Field(None, description="Allergen information")
# NEW: Local production support
produced_locally: bool = Field(False, description="If true, ingredient is produced in-house")
recipe_id: Optional[str] = Field(None, description="Recipe ID for BOM explosion (if produced locally)")
@validator('reorder_point')
def validate_reorder_point(cls, v, values):
# Only validate if both values are provided and not None
low_stock = values.get('low_stock_threshold')
if v is not None and low_stock is not None:
try:
if v <= low_stock:
raise ValueError('Reorder point must be greater than low stock threshold')
except TypeError:
# Skip validation if comparison fails due to type mismatch
pass
return v
class IngredientUpdate(InventoryBaseSchema):
"""Schema for updating ingredients and finished products"""
name: Optional[str] = Field(None, max_length=255, description="Product name")
product_type: Optional[ProductType] = Field(None, description="Type of product (ingredient or finished_product)")
sku: Optional[str] = Field(None, max_length=100, description="SKU code")
barcode: Optional[str] = Field(None, max_length=50, description="Barcode")
category: Optional[str] = Field(None, description="Product category")
subcategory: Optional[str] = Field(None, max_length=100, description="Subcategory")
description: Optional[str] = Field(None, description="Ingredient description")
brand: Optional[str] = Field(None, max_length=100, description="Brand name")
unit_of_measure: Optional[UnitOfMeasure] = Field(None, description="Unit of measure")
package_size: Optional[float] = Field(None, gt=0, description="Package size")
# Pricing
average_cost: Optional[Decimal] = Field(None, ge=0, description="Average cost per unit")
standard_cost: Optional[Decimal] = Field(None, ge=0, description="Standard cost per unit")
# Stock management
low_stock_threshold: Optional[float] = Field(None, ge=0, description="Low stock alert threshold")
reorder_point: Optional[float] = Field(None, ge=0, description="Reorder point")
reorder_quantity: Optional[float] = Field(None, gt=0, description="Default reorder quantity")
max_stock_level: Optional[float] = Field(None, gt=0, description="Maximum stock level")
# Shelf life (default value only - actual per batch)
shelf_life_days: Optional[int] = Field(None, gt=0, description="Default shelf life in days")
# Properties
is_active: Optional[bool] = Field(None, description="Is active")
is_perishable: Optional[bool] = Field(None, description="Is perishable")
allergen_info: Optional[Dict[str, Any]] = Field(None, description="Allergen information")
# NEW: Local production support
produced_locally: Optional[bool] = Field(None, description="If true, ingredient is produced in-house")
recipe_id: Optional[str] = Field(None, description="Recipe ID for BOM explosion (if produced locally)")
class IngredientResponse(InventoryBaseSchema):
"""Schema for ingredient and finished product API responses"""
id: str
tenant_id: str
name: str
product_type: ProductType
sku: Optional[str]
barcode: Optional[str]
category: Optional[str] # Will be populated from ingredient_category or product_category
subcategory: Optional[str]
description: Optional[str]
brand: Optional[str]
unit_of_measure: UnitOfMeasure
package_size: Optional[float]
average_cost: Optional[float]
last_purchase_price: Optional[float]
standard_cost: Optional[float]
low_stock_threshold: Optional[float] # Now optional
reorder_point: Optional[float] # Now optional
reorder_quantity: Optional[float] # Now optional
max_stock_level: Optional[float]
shelf_life_days: Optional[int] # Default value only
is_active: bool
is_perishable: bool
allergen_info: Optional[Dict[str, Any]]
# NEW: Local production support
produced_locally: bool = False
recipe_id: Optional[str] = None
created_at: datetime
updated_at: datetime
created_by: Optional[str]
# Computed fields
current_stock: Optional[float] = None
is_low_stock: Optional[bool] = None
needs_reorder: Optional[bool] = None
@validator('allergen_info', pre=True)
def validate_allergen_info(cls, v):
"""Convert empty lists or lists to empty dict, handle None"""
if v is None:
return None
if isinstance(v, list):
# If it's an empty list, return None; if it's a non-empty list, convert to dict format
return {"allergens": v} if v else None
if isinstance(v, dict):
return v
# For any other type including invalid ones, return None
return None
# ===== BULK INGREDIENT SCHEMAS =====
class BulkIngredientCreate(InventoryBaseSchema):
"""Schema for bulk creating ingredients"""
ingredients: List[IngredientCreate] = Field(..., description="List of ingredients to create")
class BulkIngredientResult(InventoryBaseSchema):
"""Schema for individual result in bulk operation"""
index: int = Field(..., description="Index of the ingredient in the original request")
success: bool = Field(..., description="Whether the creation succeeded")
ingredient: Optional[IngredientResponse] = Field(None, description="Created ingredient (if successful)")
error: Optional[str] = Field(None, description="Error message (if failed)")
class BulkIngredientResponse(InventoryBaseSchema):
"""Schema for bulk ingredient creation response"""
total_requested: int = Field(..., description="Total number of ingredients requested")
total_created: int = Field(..., description="Number of ingredients successfully created")
total_failed: int = Field(..., description="Number of ingredients that failed")
results: List[BulkIngredientResult] = Field(..., description="Detailed results for each ingredient")
transaction_id: str = Field(..., description="Transaction ID for audit trail")
# ===== STOCK SCHEMAS =====
class StockCreate(InventoryBaseSchema):
"""Schema for creating stock entries"""
ingredient_id: str = Field(..., description="Ingredient ID")
supplier_id: Optional[str] = Field(None, description="Supplier ID")
batch_number: Optional[str] = Field(None, max_length=100, description="Batch number")
lot_number: Optional[str] = Field(None, max_length=100, description="Lot number")
supplier_batch_ref: Optional[str] = Field(None, max_length=100, description="Supplier batch reference")
# Production stage tracking
production_stage: ProductionStage = Field(default=ProductionStage.RAW_INGREDIENT, description="Production stage of the stock")
transformation_reference: Optional[str] = Field(None, max_length=100, description="Transformation reference ID")
current_quantity: float = Field(..., ge=0, description="Current quantity")
received_date: Optional[datetime] = Field(None, description="Date received")
expiration_date: Optional[datetime] = Field(None, description="Expiration date")
best_before_date: Optional[datetime] = Field(None, description="Best before date")
# Stage-specific expiration fields
original_expiration_date: Optional[datetime] = Field(None, description="Original batch expiration (for par-baked items)")
transformation_date: Optional[datetime] = Field(None, description="Date when product was transformed")
final_expiration_date: Optional[datetime] = Field(None, description="Final expiration after transformation")
unit_cost: Optional[Decimal] = Field(None, ge=0, description="Unit cost")
storage_location: Optional[str] = Field(None, max_length=100, description="Storage location")
warehouse_zone: Optional[str] = Field(None, max_length=50, description="Warehouse zone")
shelf_position: Optional[str] = Field(None, max_length=50, description="Shelf position")
quality_status: str = Field("good", description="Quality status")
# Batch-specific storage requirements
requires_refrigeration: bool = Field(False, description="Requires refrigeration")
requires_freezing: bool = Field(False, description="Requires freezing")
storage_temperature_min: Optional[float] = Field(None, description="Min storage temperature (°C)")
storage_temperature_max: Optional[float] = Field(None, description="Max storage temperature (°C)")
storage_humidity_max: Optional[float] = Field(None, ge=0, le=100, description="Max humidity (%)")
shelf_life_days: Optional[int] = Field(None, gt=0, description="Batch-specific shelf life in days")
storage_instructions: Optional[str] = Field(None, description="Batch-specific storage instructions")
@validator('ingredient_id')
def validate_ingredient_id(cls, v):
"""Validate ingredient_id is a valid UUID"""
if not v:
raise ValueError("ingredient_id is required")
if isinstance(v, str):
try:
# Validate it's a proper UUID
UUID(v)
return v
except (ValueError, AttributeError) as e:
raise ValueError(f"ingredient_id must be a valid UUID string, got: {v}")
return str(v)
@validator('supplier_id')
def validate_supplier_id(cls, v):
"""Convert empty string to None for optional UUID field"""
if v == '' or (isinstance(v, str) and v.strip() == ''):
return None
return v
@validator('storage_temperature_max')
def validate_temperature_range(cls, v, values):
# Only validate if both values are provided and not None
min_temp = values.get('storage_temperature_min')
if v is not None and min_temp is not None:
try:
if v <= min_temp:
raise ValueError('Max temperature must be greater than min temperature')
except TypeError:
# Skip validation if comparison fails due to type mismatch
pass
return v
class StockUpdate(InventoryBaseSchema):
"""Schema for updating stock entries"""
supplier_id: Optional[str] = Field(None, description="Supplier ID")
batch_number: Optional[str] = Field(None, max_length=100, description="Batch number")
lot_number: Optional[str] = Field(None, max_length=100, description="Lot number")
supplier_batch_ref: Optional[str] = Field(None, max_length=100, description="Supplier batch reference")
# Production stage tracking
production_stage: Optional[ProductionStage] = Field(None, description="Production stage of the stock")
transformation_reference: Optional[str] = Field(None, max_length=100, description="Transformation reference ID")
current_quantity: Optional[float] = Field(None, ge=0, description="Current quantity")
reserved_quantity: Optional[float] = Field(None, ge=0, description="Reserved quantity")
received_date: Optional[datetime] = Field(None, description="Date received")
expiration_date: Optional[datetime] = Field(None, description="Expiration date")
best_before_date: Optional[datetime] = Field(None, description="Best before date")
# Stage-specific expiration fields
original_expiration_date: Optional[datetime] = Field(None, description="Original batch expiration (for par-baked items)")
transformation_date: Optional[datetime] = Field(None, description="Date when product was transformed")
final_expiration_date: Optional[datetime] = Field(None, description="Final expiration after transformation")
unit_cost: Optional[Decimal] = Field(None, ge=0, description="Unit cost")
storage_location: Optional[str] = Field(None, max_length=100, description="Storage location")
warehouse_zone: Optional[str] = Field(None, max_length=50, description="Warehouse zone")
shelf_position: Optional[str] = Field(None, max_length=50, description="Shelf position")
is_available: Optional[bool] = Field(None, description="Is available")
quality_status: Optional[str] = Field(None, description="Quality status")
# Batch-specific storage requirements
requires_refrigeration: Optional[bool] = Field(None, description="Requires refrigeration")
requires_freezing: Optional[bool] = Field(None, description="Requires freezing")
storage_temperature_min: Optional[float] = Field(None, description="Min storage temperature (°C)")
storage_temperature_max: Optional[float] = Field(None, description="Max storage temperature (°C)")
storage_humidity_max: Optional[float] = Field(None, ge=0, le=100, description="Max humidity (%)")
shelf_life_days: Optional[int] = Field(None, gt=0, description="Batch-specific shelf life in days")
storage_instructions: Optional[str] = Field(None, description="Batch-specific storage instructions")
@validator('supplier_id')
def validate_supplier_id(cls, v):
"""Convert empty string to None for optional UUID field"""
if v == '' or (isinstance(v, str) and v.strip() == ''):
return None
return v
class StockResponse(InventoryBaseSchema):
"""Schema for stock API responses"""
id: str
tenant_id: str
ingredient_id: str
supplier_id: Optional[str]
batch_number: Optional[str]
lot_number: Optional[str]
supplier_batch_ref: Optional[str]
# Production stage tracking
production_stage: ProductionStage
transformation_reference: Optional[str]
current_quantity: float
reserved_quantity: float
available_quantity: float
received_date: Optional[datetime]
expiration_date: Optional[datetime]
best_before_date: Optional[datetime]
# Stage-specific expiration fields
original_expiration_date: Optional[datetime]
transformation_date: Optional[datetime]
final_expiration_date: Optional[datetime]
unit_cost: Optional[float]
total_cost: Optional[float]
storage_location: Optional[str]
warehouse_zone: Optional[str]
shelf_position: Optional[str]
is_available: bool
is_expired: bool
quality_status: str
# Batch-specific storage requirements
requires_refrigeration: bool
requires_freezing: bool
storage_temperature_min: Optional[float]
storage_temperature_max: Optional[float]
storage_humidity_max: Optional[float]
shelf_life_days: Optional[int]
storage_instructions: Optional[str]
created_at: datetime
updated_at: datetime
# Related data
ingredient: Optional[IngredientResponse] = None
# ===== BULK STOCK SCHEMAS =====
class BulkStockCreate(InventoryBaseSchema):
"""Schema for bulk creating stock entries"""
stocks: List[StockCreate] = Field(..., description="List of stock entries to create")
class BulkStockResult(InventoryBaseSchema):
"""Schema for individual result in bulk stock operation"""
index: int = Field(..., description="Index of the stock in the original request")
success: bool = Field(..., description="Whether the creation succeeded")
stock: Optional[StockResponse] = Field(None, description="Created stock (if successful)")
error: Optional[str] = Field(None, description="Error message (if failed)")
class BulkStockResponse(InventoryBaseSchema):
"""Schema for bulk stock creation response"""
total_requested: int = Field(..., description="Total number of stock entries requested")
total_created: int = Field(..., description="Number of stock entries successfully created")
total_failed: int = Field(..., description="Number of stock entries that failed")
results: List[BulkStockResult] = Field(..., description="Detailed results for each stock entry")
transaction_id: str = Field(..., description="Transaction ID for audit trail")
# ===== STOCK MOVEMENT SCHEMAS =====
class StockMovementCreate(InventoryBaseSchema):
"""Schema for creating stock movements"""
ingredient_id: str = Field(..., description="Ingredient ID")
stock_id: Optional[str] = Field(None, description="Stock ID")
movement_type: StockMovementType = Field(..., description="Movement type")
quantity: float = Field(..., description="Quantity moved")
unit_cost: Optional[Decimal] = Field(None, ge=0, description="Unit cost")
reference_number: Optional[str] = Field(None, max_length=100, description="Reference number")
supplier_id: Optional[str] = Field(None, description="Supplier ID")
notes: Optional[str] = Field(None, description="Movement notes")
reason_code: Optional[str] = Field(None, max_length=50, description="Reason code")
movement_date: Optional[datetime] = Field(None, description="Movement date")
@validator('ingredient_id')
def validate_ingredient_id(cls, v):
"""Validate ingredient_id is a valid UUID"""
if not v:
raise ValueError("ingredient_id is required")
if isinstance(v, str):
try:
# Validate it's a proper UUID
UUID(v)
return v
except (ValueError, AttributeError) as e:
raise ValueError(f"ingredient_id must be a valid UUID string, got: {v}")
return str(v)
class StockMovementResponse(InventoryBaseSchema):
"""Schema for stock movement API responses"""
id: str
tenant_id: str
ingredient_id: str
stock_id: Optional[str]
movement_type: StockMovementType
quantity: float
unit_cost: Optional[float]
total_cost: Optional[float]
quantity_before: Optional[float]
quantity_after: Optional[float]
reference_number: Optional[str]
supplier_id: Optional[str]
notes: Optional[str]
reason_code: Optional[str]
movement_date: datetime
created_at: datetime
created_by: Optional[str]
# Related data
ingredient: Optional[IngredientResponse] = None
# ===== PRODUCT TRANSFORMATION SCHEMAS =====
class ProductTransformationCreate(InventoryBaseSchema):
"""Schema for creating product transformations"""
source_ingredient_id: str = Field(..., description="Source ingredient ID")
target_ingredient_id: str = Field(..., description="Target ingredient ID")
source_stage: ProductionStage = Field(..., description="Source production stage")
target_stage: ProductionStage = Field(..., description="Target production stage")
source_quantity: float = Field(..., gt=0, description="Input quantity")
target_quantity: float = Field(..., gt=0, description="Output quantity")
conversion_ratio: Optional[float] = Field(None, gt=0, description="Conversion ratio (auto-calculated if not provided)")
# Expiration handling
expiration_calculation_method: str = Field("days_from_transformation", description="How to calculate expiration")
expiration_days_offset: Optional[int] = Field(1, description="Days from transformation date for expiration")
# Process details
process_notes: Optional[str] = Field(None, description="Process notes")
target_batch_number: Optional[str] = Field(None, max_length=100, description="Target batch number")
# Source stock selection (optional - if not provided, uses FIFO)
source_stock_ids: Optional[List[str]] = Field(None, description="Specific source stock IDs to transform")
@validator('source_ingredient_id', 'target_ingredient_id')
def validate_ingredient_ids(cls, v):
"""Validate ingredient IDs are valid UUIDs"""
if not v:
raise ValueError("ingredient_id is required")
if isinstance(v, str):
try:
# Validate it's a proper UUID
UUID(v)
return v
except (ValueError, AttributeError) as e:
raise ValueError(f"ingredient_id must be a valid UUID string, got: {v}")
return str(v)
class ProductTransformationResponse(InventoryBaseSchema):
"""Schema for product transformation responses"""
id: str
tenant_id: str
transformation_reference: str
source_ingredient_id: str
target_ingredient_id: str
source_stage: ProductionStage
target_stage: ProductionStage
source_quantity: float
target_quantity: float
conversion_ratio: float
expiration_calculation_method: str
expiration_days_offset: Optional[int]
transformation_date: datetime
process_notes: Optional[str]
performed_by: Optional[str]
source_batch_numbers: Optional[str]
target_batch_number: Optional[str]
is_completed: bool
is_reversed: bool
created_at: datetime
created_by: Optional[str]
# Related data
source_ingredient: Optional[IngredientResponse] = None
target_ingredient: Optional[IngredientResponse] = None
# ===== ALERT SCHEMAS =====
class StockAlertResponse(InventoryBaseSchema):
"""Schema for stock alert API responses"""
id: str
tenant_id: str
ingredient_id: str
stock_id: Optional[str]
alert_type: str
severity: str
title: str
message: str
current_quantity: Optional[float]
threshold_value: Optional[float]
expiration_date: Optional[datetime]
is_active: bool
is_acknowledged: bool
acknowledged_by: Optional[str]
acknowledged_at: Optional[datetime]
is_resolved: bool
resolved_by: Optional[str]
resolved_at: Optional[datetime]
resolution_notes: Optional[str]
created_at: datetime
updated_at: datetime
# Related data
ingredient: Optional[IngredientResponse] = None
# ===== DASHBOARD AND SUMMARY SCHEMAS =====
class InventorySummary(InventoryBaseSchema):
"""Inventory dashboard summary"""
total_ingredients: int
total_stock_value: float
low_stock_alerts: int
expiring_soon_items: int
expired_items: int
out_of_stock_items: int
# By category
stock_by_category: Dict[str, Dict[str, Any]]
# Recent activity
recent_movements: int
recent_purchases: int
recent_waste: int
class StockLevelSummary(InventoryBaseSchema):
"""Stock level summary for an ingredient"""
ingredient_id: str
ingredient_name: str
unit_of_measure: str
total_quantity: float
available_quantity: float
reserved_quantity: float
# Status indicators
is_low_stock: bool
needs_reorder: bool
has_expired_stock: bool
# Batch information
total_batches: int
oldest_batch_date: Optional[datetime]
newest_batch_date: Optional[datetime]
next_expiration_date: Optional[datetime]
# Cost information
average_unit_cost: Optional[float]
total_stock_value: Optional[float]
# ===== REQUEST/RESPONSE WRAPPER SCHEMAS =====
class PaginatedResponse(BaseModel, Generic[T]):
"""Generic paginated response"""
items: List[T]
total: int
page: int
size: int
pages: int
class Config:
from_attributes = True
class InventoryFilter(BaseModel):
"""Inventory filtering parameters"""
category: Optional[IngredientCategory] = None
is_active: Optional[bool] = None
is_low_stock: Optional[bool] = None
needs_reorder: Optional[bool] = None
search: Optional[str] = None
class StockFilter(BaseModel):
"""Stock filtering parameters"""
ingredient_id: Optional[str] = None
production_stage: Optional[ProductionStage] = None
transformation_reference: Optional[str] = None
is_available: Optional[bool] = None
is_expired: Optional[bool] = None
expiring_within_days: Optional[int] = None
storage_location: Optional[str] = None
quality_status: Optional[str] = None
# Type aliases for paginated responses
IngredientListResponse = PaginatedResponse[IngredientResponse]
StockListResponse = PaginatedResponse[StockResponse]
StockMovementListResponse = PaginatedResponse[StockMovementResponse]
StockAlertListResponse = PaginatedResponse[StockAlertResponse]

View File

@@ -0,0 +1,217 @@
# ================================================================
# services/inventory/app/schemas/sustainability.py
# ================================================================
"""
Sustainability Schemas - Environmental Impact & SDG Compliance
"""
from datetime import datetime
from typing import Dict, Any, List, Optional
from decimal import Decimal
from pydantic import BaseModel, Field
class PeriodInfo(BaseModel):
"""Time period for metrics"""
start_date: str
end_date: str
days: int
class WasteMetrics(BaseModel):
"""Waste tracking metrics"""
total_waste_kg: float = Field(description="Total waste in kilograms")
production_waste_kg: float = Field(description="Waste from production processes")
expired_waste_kg: float = Field(description="Waste from expired inventory")
waste_percentage: float = Field(description="Waste as percentage of total production")
waste_by_reason: Dict[str, float] = Field(description="Breakdown by waste reason")
class CO2Emissions(BaseModel):
"""CO2 emission metrics"""
kg: float = Field(description="CO2 emissions in kilograms")
tons: float = Field(description="CO2 emissions in tons")
trees_to_offset: float = Field(description="Equivalent trees needed to offset emissions")
class WaterFootprint(BaseModel):
"""Water usage metrics"""
liters: float = Field(description="Water footprint in liters")
cubic_meters: float = Field(description="Water footprint in cubic meters")
class LandUse(BaseModel):
"""Land use metrics"""
square_meters: float = Field(description="Land use in square meters")
hectares: float = Field(description="Land use in hectares")
class HumanEquivalents(BaseModel):
"""Human-relatable equivalents for impact"""
car_km_equivalent: float = Field(description="Equivalent kilometers driven by car")
smartphone_charges: float = Field(description="Equivalent smartphone charges")
showers_equivalent: float = Field(description="Equivalent showers taken")
trees_planted: float = Field(description="Equivalent trees planted")
class EnvironmentalImpact(BaseModel):
"""Environmental impact of food waste"""
co2_emissions: CO2Emissions
water_footprint: WaterFootprint
land_use: LandUse
human_equivalents: HumanEquivalents
class SDG123Metrics(BaseModel):
"""UN SDG 12.3 specific metrics"""
baseline_waste_percentage: float = Field(description="Baseline waste percentage")
current_waste_percentage: float = Field(description="Current waste percentage")
reduction_achieved: float = Field(description="Reduction achieved from baseline (%)")
target_reduction: float = Field(description="Target reduction (50%)", default=50.0)
progress_to_target: float = Field(description="Progress toward target (%)")
status: str = Field(description="Status code: sdg_compliant, on_track, progressing, baseline")
status_label: str = Field(description="Human-readable status")
target_waste_percentage: float = Field(description="Target waste percentage to achieve")
class SDGCompliance(BaseModel):
"""SDG compliance assessment"""
sdg_12_3: SDG123Metrics
baseline_period: str = Field(description="Period used for baseline calculation")
certification_ready: bool = Field(description="Ready for SDG certification")
improvement_areas: List[str] = Field(description="Identified areas for improvement")
class EnvironmentalImpactAvoided(BaseModel):
"""Environmental impact avoided through AI"""
co2_kg: float = Field(description="CO2 emissions avoided (kg)")
water_liters: float = Field(description="Water saved (liters)")
class AvoidedWaste(BaseModel):
"""Waste avoided through AI predictions"""
waste_avoided_kg: float = Field(description="Waste avoided in kilograms")
ai_assisted_batches: int = Field(description="Number of AI-assisted batches")
environmental_impact_avoided: EnvironmentalImpactAvoided
methodology: str = Field(description="Calculation methodology")
class FinancialImpact(BaseModel):
"""Financial impact of waste"""
waste_cost_eur: float = Field(description="Cost of waste in euros")
cost_per_kg: float = Field(description="Average cost per kg")
potential_monthly_savings: float = Field(description="Potential monthly savings")
annual_projection: float = Field(description="Annual cost projection")
class GrantProgramEligibility(BaseModel):
"""Eligibility for a specific grant program"""
eligible: bool = Field(description="Whether eligible for this grant")
confidence: str = Field(description="Confidence level: high, medium, low")
requirements_met: bool = Field(description="Whether requirements are met")
funding_eur: float = Field(description="Available funding in euros")
deadline: str = Field(description="Application deadline")
program_type: str = Field(description="Type: grant, loan, or certification")
sector_specific: Optional[str] = Field(None, description="Sector if specific: bakery, retail, etc.")
class SpainCompliance(BaseModel):
"""Spain-specific legal compliance"""
law_1_2025: bool = Field(description="Compliance with Spanish Law 1/2025 on food waste")
circular_economy_strategy: bool = Field(description="Aligned with Spanish Circular Economy Strategy")
class GrantReadiness(BaseModel):
"""Grant application readiness assessment"""
overall_readiness_percentage: float = Field(description="Overall readiness percentage")
grant_programs: Dict[str, GrantProgramEligibility] = Field(description="Eligibility by program")
recommended_applications: List[str] = Field(description="Recommended grant programs to apply for")
spain_compliance: SpainCompliance = Field(description="Spain-specific compliance status")
class SustainabilityMetrics(BaseModel):
"""Complete sustainability metrics response"""
period: PeriodInfo
waste_metrics: WasteMetrics
environmental_impact: EnvironmentalImpact
sdg_compliance: SDGCompliance
avoided_waste: AvoidedWaste
financial_impact: FinancialImpact
grant_readiness: GrantReadiness
class BaselineComparison(BaseModel):
"""Baseline comparison for grants"""
baseline: float
current: float
improvement: float
class SupportingData(BaseModel):
"""Supporting data for grant applications"""
baseline_comparison: BaselineComparison
environmental_benefits: EnvironmentalImpact
financial_benefits: FinancialImpact
class Certifications(BaseModel):
"""Certification status"""
sdg_12_3_compliant: bool
grant_programs_eligible: List[str]
class ExecutiveSummary(BaseModel):
"""Executive summary for grant reports"""
total_waste_reduced_kg: float
waste_reduction_percentage: float
co2_emissions_avoided_kg: float
financial_savings_eur: float
sdg_compliance_status: str
class ReportMetadata(BaseModel):
"""Report metadata"""
generated_at: str
report_type: str
period: PeriodInfo
tenant_id: str
class GrantReport(BaseModel):
"""Complete grant application report"""
report_metadata: ReportMetadata
executive_summary: ExecutiveSummary
detailed_metrics: SustainabilityMetrics
certifications: Certifications
supporting_data: SupportingData
# Request schemas
class SustainabilityMetricsRequest(BaseModel):
"""Request for sustainability metrics"""
start_date: Optional[datetime] = Field(None, description="Start date for metrics")
end_date: Optional[datetime] = Field(None, description="End date for metrics")
class GrantReportRequest(BaseModel):
"""Request for grant report export"""
grant_type: str = Field("general", description="Type of grant: general, eu_horizon, farm_to_fork, etc.")
start_date: Optional[datetime] = Field(None, description="Start date for report")
end_date: Optional[datetime] = Field(None, description="End date for report")
format: str = Field("json", description="Export format: json, pdf, csv")
# Widget/Dashboard schemas
class SustainabilityWidgetData(BaseModel):
"""Simplified data for dashboard widgets"""
total_waste_kg: float
waste_reduction_percentage: float
co2_saved_kg: float
water_saved_liters: float
trees_equivalent: float
sdg_status: str
sdg_progress: float
grant_programs_ready: int
financial_savings_eur: float