Files
bakery-ia/services/procurement/app/schemas/replenishment.py
2025-10-30 21:08:07 +01:00

441 lines
12 KiB
Python

"""
Pydantic schemas for replenishment planning.
"""
from pydantic import BaseModel, Field, validator
from typing import List, Optional, Dict, Any
from datetime import date, datetime
from decimal import Decimal
from uuid import UUID
# ============================================================================
# Replenishment Plan Schemas
# ============================================================================
class ReplenishmentPlanItemBase(BaseModel):
"""Base schema for replenishment plan item"""
ingredient_id: UUID
ingredient_name: str
unit_of_measure: str
base_quantity: Decimal
safety_stock_quantity: Decimal
shelf_life_adjusted_quantity: Decimal
final_order_quantity: Decimal
order_date: date
delivery_date: date
required_by_date: date
lead_time_days: int
is_urgent: bool
urgency_reason: Optional[str] = None
waste_risk: str
stockout_risk: str
supplier_id: Optional[UUID] = None
safety_stock_calculation: Optional[Dict[str, Any]] = None
shelf_life_adjustment: Optional[Dict[str, Any]] = None
inventory_projection: Optional[Dict[str, Any]] = None
class ReplenishmentPlanItemCreate(ReplenishmentPlanItemBase):
"""Schema for creating replenishment plan item"""
replenishment_plan_id: UUID
class ReplenishmentPlanItemResponse(ReplenishmentPlanItemBase):
"""Schema for replenishment plan item response"""
id: UUID
replenishment_plan_id: UUID
created_at: datetime
class Config:
from_attributes = True
class ReplenishmentPlanBase(BaseModel):
"""Base schema for replenishment plan"""
planning_date: date
projection_horizon_days: int = 7
forecast_id: Optional[UUID] = None
production_schedule_id: Optional[UUID] = None
total_items: int
urgent_items: int
high_risk_items: int
total_estimated_cost: Decimal
class ReplenishmentPlanCreate(ReplenishmentPlanBase):
"""Schema for creating replenishment plan"""
tenant_id: UUID
items: List[Dict[str, Any]] = []
class ReplenishmentPlanResponse(ReplenishmentPlanBase):
"""Schema for replenishment plan response"""
id: UUID
tenant_id: UUID
status: str
created_at: datetime
updated_at: Optional[datetime] = None
executed_at: Optional[datetime] = None
items: List[ReplenishmentPlanItemResponse] = []
class Config:
from_attributes = True
class ReplenishmentPlanSummary(BaseModel):
"""Summary schema for list views"""
id: UUID
tenant_id: UUID
planning_date: date
total_items: int
urgent_items: int
high_risk_items: int
total_estimated_cost: Decimal
status: str
created_at: datetime
class Config:
from_attributes = True
# ============================================================================
# Inventory Projection Schemas
# ============================================================================
class InventoryProjectionBase(BaseModel):
"""Base schema for inventory projection"""
ingredient_id: UUID
ingredient_name: str
projection_date: date
starting_stock: Decimal
forecasted_consumption: Decimal
scheduled_receipts: Decimal
projected_ending_stock: Decimal
is_stockout: bool
coverage_gap: Decimal
class InventoryProjectionCreate(InventoryProjectionBase):
"""Schema for creating inventory projection"""
tenant_id: UUID
replenishment_plan_id: Optional[UUID] = None
class InventoryProjectionResponse(InventoryProjectionBase):
"""Schema for inventory projection response"""
id: UUID
tenant_id: UUID
replenishment_plan_id: Optional[UUID] = None
created_at: datetime
class Config:
from_attributes = True
class IngredientProjectionSummary(BaseModel):
"""Summary of projections for one ingredient"""
ingredient_id: UUID
ingredient_name: str
current_stock: Decimal
unit_of_measure: str
projection_horizon_days: int
total_consumption: Decimal
total_receipts: Decimal
stockout_days: int
stockout_risk: str
daily_projections: List[Dict[str, Any]]
# ============================================================================
# Supplier Allocation Schemas
# ============================================================================
class SupplierAllocationBase(BaseModel):
"""Base schema for supplier allocation"""
supplier_id: UUID
supplier_name: str
allocation_type: str
allocated_quantity: Decimal
allocation_percentage: Decimal
unit_price: Decimal
total_cost: Decimal
lead_time_days: int
supplier_score: Decimal
score_breakdown: Optional[Dict[str, float]] = None
allocation_reason: Optional[str] = None
class SupplierAllocationCreate(SupplierAllocationBase):
"""Schema for creating supplier allocation"""
replenishment_plan_item_id: Optional[UUID] = None
requirement_id: Optional[UUID] = None
class SupplierAllocationResponse(SupplierAllocationBase):
"""Schema for supplier allocation response"""
id: UUID
replenishment_plan_item_id: Optional[UUID] = None
requirement_id: Optional[UUID] = None
created_at: datetime
class Config:
from_attributes = True
# ============================================================================
# Supplier Selection Schemas
# ============================================================================
class SupplierSelectionRequest(BaseModel):
"""Request to select suppliers for an ingredient"""
ingredient_id: UUID
ingredient_name: str
required_quantity: Decimal
supplier_options: List[Dict[str, Any]]
class SupplierSelectionResult(BaseModel):
"""Result of supplier selection"""
ingredient_id: UUID
ingredient_name: str
required_quantity: Decimal
allocations: List[Dict[str, Any]]
total_cost: Decimal
weighted_lead_time: float
risk_score: float
diversification_applied: bool
selection_strategy: str
# ============================================================================
# Replenishment Planning Request Schemas
# ============================================================================
class IngredientRequirementInput(BaseModel):
"""Input for a single ingredient requirement"""
ingredient_id: UUID
ingredient_name: str
required_quantity: Decimal
required_by_date: date
supplier_id: Optional[UUID] = None
lead_time_days: int = 3
shelf_life_days: Optional[int] = None
is_perishable: bool = False
category: str = 'dry'
unit_of_measure: str = 'kg'
current_stock: Decimal = Decimal('0')
daily_consumption_rate: float = 0.0
demand_std_dev: float = 0.0
class GenerateReplenishmentPlanRequest(BaseModel):
"""Request to generate replenishment plan"""
tenant_id: UUID
requirements: List[IngredientRequirementInput]
forecast_id: Optional[UUID] = None
production_schedule_id: Optional[UUID] = None
projection_horizon_days: int = 7
service_level: float = 0.95
buffer_days: int = 1
class GenerateReplenishmentPlanResponse(BaseModel):
"""Response from generating replenishment plan"""
plan_id: UUID
tenant_id: UUID
planning_date: date
projection_horizon_days: int
total_items: int
urgent_items: int
high_risk_items: int
total_estimated_cost: Decimal
created_at: datetime
items: List[Dict[str, Any]]
# ============================================================================
# MOQ Aggregation Schemas
# ============================================================================
class MOQAggregationRequest(BaseModel):
"""Request for MOQ aggregation"""
requirements: List[Dict[str, Any]]
supplier_constraints: Dict[str, Dict[str, Any]]
class MOQAggregationResponse(BaseModel):
"""Response from MOQ aggregation"""
aggregated_orders: List[Dict[str, Any]]
efficiency_metrics: Dict[str, Any]
# ============================================================================
# Safety Stock Calculation Schemas
# ============================================================================
class SafetyStockRequest(BaseModel):
"""Request for safety stock calculation"""
ingredient_id: UUID
daily_demands: List[float]
lead_time_days: int
service_level: float = 0.95
class SafetyStockResponse(BaseModel):
"""Response from safety stock calculation"""
safety_stock_quantity: Decimal
service_level: float
z_score: float
demand_std_dev: float
lead_time_days: int
calculation_method: str
confidence: str
reasoning: str
# ============================================================================
# Inventory Projection Request Schemas
# ============================================================================
class ProjectInventoryRequest(BaseModel):
"""Request to project inventory"""
ingredient_id: UUID
ingredient_name: str
current_stock: Decimal
unit_of_measure: str
daily_demand: List[Dict[str, Any]]
scheduled_receipts: List[Dict[str, Any]] = []
projection_horizon_days: int = 7
class ProjectInventoryResponse(BaseModel):
"""Response from inventory projection"""
ingredient_id: UUID
ingredient_name: str
current_stock: Decimal
unit_of_measure: str
projection_horizon_days: int
total_consumption: Decimal
total_receipts: Decimal
stockout_days: int
stockout_risk: str
daily_projections: List[Dict[str, Any]]
# ============================================================================
# Supplier Selection History Schemas
# ============================================================================
class SupplierSelectionHistoryBase(BaseModel):
"""Base schema for supplier selection history"""
ingredient_id: UUID
ingredient_name: str
selected_supplier_id: UUID
selected_supplier_name: str
selection_date: date
quantity: Decimal
unit_price: Decimal
total_cost: Decimal
lead_time_days: int
quality_score: Optional[Decimal] = None
delivery_performance: Optional[Decimal] = None
selection_strategy: str
was_primary_choice: bool = True
class SupplierSelectionHistoryCreate(SupplierSelectionHistoryBase):
"""Schema for creating supplier selection history"""
tenant_id: UUID
class SupplierSelectionHistoryResponse(SupplierSelectionHistoryBase):
"""Schema for supplier selection history response"""
id: UUID
tenant_id: UUID
created_at: datetime
class Config:
from_attributes = True
# ============================================================================
# Analytics Schemas
# ============================================================================
class ReplenishmentAnalytics(BaseModel):
"""Analytics for replenishment planning"""
total_plans: int
total_items_planned: int
total_estimated_value: Decimal
urgent_items_percentage: float
high_risk_items_percentage: float
average_lead_time_days: float
average_safety_stock_percentage: float
stockout_prevention_rate: float
moq_optimization_savings: Decimal
supplier_diversification_rate: float
average_suppliers_per_ingredient: float
class InventoryProjectionAnalytics(BaseModel):
"""Analytics for inventory projections"""
total_ingredients: int
stockout_ingredients: int
stockout_percentage: float
risk_breakdown: Dict[str, int]
total_stockout_days: int
total_consumption: Decimal
total_receipts: Decimal
projection_horizon_days: int
# ============================================================================
# Validators
# ============================================================================
@validator('required_quantity', 'current_stock', 'allocated_quantity',
'safety_stock_quantity', 'base_quantity', 'final_order_quantity')
def validate_positive_quantity(cls, v):
"""Validate that quantities are positive"""
if v < 0:
raise ValueError('Quantity must be non-negative')
return v
@validator('service_level')
def validate_service_level(cls, v):
"""Validate service level is between 0 and 1"""
if not 0 <= v <= 1:
raise ValueError('Service level must be between 0 and 1')
return v