""" 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