441 lines
12 KiB
Python
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
|