# ================================================================ # services/procurement/app/schemas/procurement_schemas.py # ================================================================ """ Procurement Schemas - Request/response models for procurement plans Migrated from Orders Service with additions for local production support """ import uuid from datetime import datetime, date from decimal import Decimal from typing import Optional, List, Dict, Any from pydantic import BaseModel, Field, ConfigDict # ================================================================ # BASE SCHEMAS # ================================================================ class ProcurementBase(BaseModel): """Base schema for procurement entities""" model_config = ConfigDict(from_attributes=True, str_strip_whitespace=True) # ================================================================ # PROCUREMENT REQUIREMENT SCHEMAS # ================================================================ class ProcurementRequirementBase(ProcurementBase): """Base procurement requirement schema""" product_id: uuid.UUID product_name: str = Field(..., min_length=1, max_length=200) product_sku: Optional[str] = Field(None, max_length=100) product_category: Optional[str] = Field(None, max_length=100) product_type: str = Field(default="ingredient", max_length=50) required_quantity: Decimal = Field(..., gt=0) unit_of_measure: str = Field(..., min_length=1, max_length=50) safety_stock_quantity: Decimal = Field(default=Decimal("0.000"), ge=0) total_quantity_needed: Decimal = Field(..., gt=0) current_stock_level: Decimal = Field(default=Decimal("0.000"), ge=0) reserved_stock: Decimal = Field(default=Decimal("0.000"), ge=0) available_stock: Decimal = Field(default=Decimal("0.000"), ge=0) net_requirement: Decimal = Field(..., ge=0) order_demand: Decimal = Field(default=Decimal("0.000"), ge=0) production_demand: Decimal = Field(default=Decimal("0.000"), ge=0) forecast_demand: Decimal = Field(default=Decimal("0.000"), ge=0) buffer_demand: Decimal = Field(default=Decimal("0.000"), ge=0) required_by_date: date lead_time_buffer_days: int = Field(default=1, ge=0) suggested_order_date: date latest_order_date: date priority: str = Field(default="normal", pattern="^(critical|high|normal|low)$") risk_level: str = Field(default="low", pattern="^(low|medium|high|critical)$") preferred_supplier_id: Optional[uuid.UUID] = None backup_supplier_id: Optional[uuid.UUID] = None supplier_name: Optional[str] = Field(None, max_length=200) supplier_lead_time_days: Optional[int] = Field(None, ge=0) minimum_order_quantity: Optional[Decimal] = Field(None, ge=0) estimated_unit_cost: Optional[Decimal] = Field(None, ge=0) estimated_total_cost: Optional[Decimal] = Field(None, ge=0) last_purchase_cost: Optional[Decimal] = Field(None, ge=0) class ProcurementRequirementCreate(ProcurementRequirementBase): """Schema for creating procurement requirements""" special_requirements: Optional[str] = None storage_requirements: Optional[str] = Field(None, max_length=200) shelf_life_days: Optional[int] = Field(None, gt=0) quality_specifications: Optional[Dict[str, Any]] = None procurement_notes: Optional[str] = None # Smart procurement calculation metadata calculation_method: Optional[str] = Field(None, max_length=100) ai_suggested_quantity: Optional[Decimal] = Field(None, ge=0) adjusted_quantity: Optional[Decimal] = Field(None, ge=0) adjustment_reason: Optional[str] = None price_tier_applied: Optional[Dict[str, Any]] = None supplier_minimum_applied: bool = False storage_limit_applied: bool = False reorder_rule_applied: bool = False # NEW: Local production support fields is_locally_produced: bool = False recipe_id: Optional[uuid.UUID] = None parent_requirement_id: Optional[uuid.UUID] = None bom_explosion_level: int = Field(default=0, ge=0) class ProcurementRequirementUpdate(ProcurementBase): """Schema for updating procurement requirements""" status: Optional[str] = Field(None, pattern="^(pending|approved|ordered|partially_received|received|cancelled)$") priority: Optional[str] = Field(None, pattern="^(critical|high|normal|low)$") approved_quantity: Optional[Decimal] = Field(None, ge=0) approved_cost: Optional[Decimal] = Field(None, ge=0) purchase_order_id: Optional[uuid.UUID] = None purchase_order_number: Optional[str] = Field(None, max_length=50) ordered_quantity: Optional[Decimal] = Field(None, ge=0) expected_delivery_date: Optional[date] = None actual_delivery_date: Optional[date] = None received_quantity: Optional[Decimal] = Field(None, ge=0) delivery_status: Optional[str] = Field(None, pattern="^(pending|in_transit|delivered|delayed|cancelled)$") procurement_notes: Optional[str] = None class ProcurementRequirementResponse(ProcurementRequirementBase): """Schema for procurement requirement responses""" id: uuid.UUID plan_id: uuid.UUID requirement_number: str status: str created_at: datetime updated_at: datetime purchase_order_id: Optional[uuid.UUID] = None purchase_order_number: Optional[str] = None ordered_quantity: Decimal ordered_at: Optional[datetime] = None expected_delivery_date: Optional[date] = None actual_delivery_date: Optional[date] = None received_quantity: Decimal delivery_status: str fulfillment_rate: Optional[Decimal] = None on_time_delivery: Optional[bool] = None quality_rating: Optional[Decimal] = None approved_quantity: Optional[Decimal] = None approved_cost: Optional[Decimal] = None approved_at: Optional[datetime] = None approved_by: Optional[uuid.UUID] = None special_requirements: Optional[str] = None storage_requirements: Optional[str] = None shelf_life_days: Optional[int] = None quality_specifications: Optional[Dict[str, Any]] = None procurement_notes: Optional[str] = None # Smart procurement calculation metadata calculation_method: Optional[str] = None ai_suggested_quantity: Optional[Decimal] = None adjusted_quantity: Optional[Decimal] = None adjustment_reason: Optional[str] = None price_tier_applied: Optional[Dict[str, Any]] = None supplier_minimum_applied: bool = False storage_limit_applied: bool = False reorder_rule_applied: bool = False # NEW: Local production support fields is_locally_produced: bool = False recipe_id: Optional[uuid.UUID] = None parent_requirement_id: Optional[uuid.UUID] = None bom_explosion_level: int = 0 # ================================================================ # PROCUREMENT PLAN SCHEMAS # ================================================================ class ProcurementPlanBase(ProcurementBase): """Base procurement plan schema""" plan_date: date plan_period_start: date plan_period_end: date planning_horizon_days: int = Field(default=14, gt=0) plan_type: str = Field(default="regular", pattern="^(regular|emergency|seasonal|urgent)$") priority: str = Field(default="normal", pattern="^(critical|high|normal|low)$") business_model: Optional[str] = Field(None, pattern="^(individual_bakery|central_bakery)$") procurement_strategy: str = Field(default="just_in_time", pattern="^(just_in_time|bulk|mixed|bulk_order)$") safety_stock_buffer: Decimal = Field(default=Decimal("20.00"), ge=0, le=100) supply_risk_level: str = Field(default="low", pattern="^(low|medium|high|critical)$") demand_forecast_confidence: Optional[Decimal] = Field(None, ge=1, le=10) seasonality_adjustment: Decimal = Field(default=Decimal("0.00")) special_requirements: Optional[str] = None class ProcurementPlanCreate(ProcurementPlanBase): """Schema for creating procurement plans""" tenant_id: uuid.UUID requirements: Optional[List[ProcurementRequirementCreate]] = [] class ProcurementPlanUpdate(ProcurementBase): """Schema for updating procurement plans""" status: Optional[str] = Field(None, pattern="^(draft|pending_approval|approved|in_execution|completed|cancelled)$") priority: Optional[str] = Field(None, pattern="^(critical|high|normal|low)$") approved_at: Optional[datetime] = None approved_by: Optional[uuid.UUID] = None execution_started_at: Optional[datetime] = None execution_completed_at: Optional[datetime] = None special_requirements: Optional[str] = None seasonal_adjustments: Optional[Dict[str, Any]] = None class ProcurementPlanResponse(ProcurementPlanBase): """Schema for procurement plan responses""" id: uuid.UUID tenant_id: uuid.UUID plan_number: str status: str total_requirements: int total_estimated_cost: Decimal total_approved_cost: Decimal cost_variance: Decimal total_demand_orders: int total_demand_quantity: Decimal total_production_requirements: Decimal primary_suppliers_count: int backup_suppliers_count: int supplier_diversification_score: Optional[Decimal] = None approved_at: Optional[datetime] = None approved_by: Optional[uuid.UUID] = None execution_started_at: Optional[datetime] = None execution_completed_at: Optional[datetime] = None fulfillment_rate: Optional[Decimal] = None on_time_delivery_rate: Optional[Decimal] = None cost_accuracy: Optional[Decimal] = None quality_score: Optional[Decimal] = None created_at: datetime updated_at: datetime created_by: Optional[uuid.UUID] = None updated_by: Optional[uuid.UUID] = None # NEW: Track forecast and production schedule links forecast_id: Optional[uuid.UUID] = None production_schedule_id: Optional[uuid.UUID] = None requirements: List[ProcurementRequirementResponse] = [] # ================================================================ # SUMMARY SCHEMAS # ================================================================ class ProcurementSummary(ProcurementBase): """Summary of procurement plans""" total_plans: int active_plans: int total_requirements: int pending_requirements: int critical_requirements: int total_estimated_cost: Decimal total_approved_cost: Decimal cost_variance: Decimal average_fulfillment_rate: Optional[Decimal] = None average_on_time_delivery: Optional[Decimal] = None top_suppliers: List[Dict[str, Any]] = [] critical_items: List[Dict[str, Any]] = [] class DashboardData(ProcurementBase): """Dashboard data for procurement overview""" current_plan: Optional[ProcurementPlanResponse] = None summary: ProcurementSummary upcoming_deliveries: List[Dict[str, Any]] = [] overdue_requirements: List[Dict[str, Any]] = [] low_stock_alerts: List[Dict[str, Any]] = [] performance_metrics: Dict[str, Any] = {} # ================================================================ # REQUEST SCHEMAS # ================================================================ class GeneratePlanRequest(ProcurementBase): """Request to generate procurement plan""" plan_date: Optional[date] = None force_regenerate: bool = False planning_horizon_days: int = Field(default=14, gt=0, le=30) include_safety_stock: bool = True safety_stock_percentage: Decimal = Field(default=Decimal("20.00"), ge=0, le=100) class AutoGenerateProcurementRequest(ProcurementBase): """ Request to auto-generate procurement plan (called by Orchestrator) This is the main entry point for orchestrated procurement planning. The Orchestrator calls Forecasting Service first, then passes forecast data here. NEW: Accepts cached data snapshots from Orchestrator to eliminate duplicate API calls. """ forecast_data: Dict[str, Any] = Field(..., description="Forecast data from Forecasting Service") production_schedule_id: Optional[uuid.UUID] = Field(None, description="Production schedule ID if available") target_date: Optional[date] = Field(None, description="Target date for the plan") planning_horizon_days: int = Field(default=14, gt=0, le=30) safety_stock_percentage: Decimal = Field(default=Decimal("20.00"), ge=0, le=100) auto_create_pos: bool = Field(True, description="Automatically create purchase orders") auto_approve_pos: bool = Field(False, description="Auto-approve qualifying purchase orders") # NEW: Cached data from Orchestrator inventory_data: Optional[Dict[str, Any]] = Field(None, description="Cached inventory snapshot from Orchestrator") suppliers_data: Optional[Dict[str, Any]] = Field(None, description="Cached suppliers snapshot from Orchestrator") recipes_data: Optional[Dict[str, Any]] = Field(None, description="Cached recipes snapshot from Orchestrator") class ForecastRequest(ProcurementBase): """Request parameters for demand forecasting""" target_date: date horizon_days: int = Field(default=1, gt=0, le=7) include_confidence_intervals: bool = True product_ids: Optional[List[uuid.UUID]] = None # ================================================================ # RESPONSE SCHEMAS # ================================================================ class GeneratePlanResponse(ProcurementBase): """Response from plan generation""" success: bool message: str plan: Optional[ProcurementPlanResponse] = None warnings: List[str] = [] errors: List[str] = [] class AutoGenerateProcurementResponse(ProcurementBase): """Response from auto-generate procurement (called by Orchestrator)""" success: bool message: str plan_id: Optional[uuid.UUID] = None plan_number: Optional[str] = None requirements_created: int = 0 purchase_orders_created: int = 0 purchase_orders_auto_approved: int = 0 total_estimated_cost: Decimal = Decimal("0") warnings: List[str] = [] errors: List[str] = [] created_pos: List[Dict[str, Any]] = [] class PaginatedProcurementPlans(ProcurementBase): """Paginated list of procurement plans""" plans: List[ProcurementPlanResponse] total: int page: int limit: int has_more: bool