Files
bakery-ia/services/procurement/app/schemas/procurement_schemas.py

369 lines
14 KiB
Python
Raw Normal View History

2025-08-23 19:47:08 +02:00
# ================================================================
2025-10-30 21:08:07 +01:00
# services/procurement/app/schemas/procurement_schemas.py
2025-08-23 19:47:08 +02:00
# ================================================================
"""
Procurement Schemas - Request/response models for procurement plans
2025-10-30 21:08:07 +01:00
Migrated from Orders Service with additions for local production support
2025-08-23 19:47:08 +02:00
"""
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)
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
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)
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
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)
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
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)
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
required_by_date: date
lead_time_buffer_days: int = Field(default=1, ge=0)
suggested_order_date: date
latest_order_date: date
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
priority: str = Field(default="normal", pattern="^(critical|high|normal|low)$")
risk_level: str = Field(default="low", pattern="^(low|medium|high|critical)$")
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
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)
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
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
2025-10-27 16:33:26 +01:00
# 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
2025-10-30 21:08:07 +01:00
# 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)
2025-08-23 19:47:08 +02:00
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)$")
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
approved_quantity: Optional[Decimal] = Field(None, ge=0)
approved_cost: Optional[Decimal] = Field(None, ge=0)
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
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)
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
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)$")
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
procurement_notes: Optional[str] = None
class ProcurementRequirementResponse(ProcurementRequirementBase):
"""Schema for procurement requirement responses"""
id: uuid.UUID
plan_id: uuid.UUID
requirement_number: str
2025-10-27 16:33:26 +01:00
2025-08-23 19:47:08 +02:00
status: str
created_at: datetime
updated_at: datetime
2025-10-27 16:33:26 +01:00
2025-08-23 19:47:08 +02:00
purchase_order_id: Optional[uuid.UUID] = None
purchase_order_number: Optional[str] = None
ordered_quantity: Decimal
ordered_at: Optional[datetime] = None
2025-10-27 16:33:26 +01:00
2025-08-23 19:47:08 +02:00
expected_delivery_date: Optional[date] = None
actual_delivery_date: Optional[date] = None
received_quantity: Decimal
delivery_status: str
2025-10-27 16:33:26 +01:00
2025-08-23 19:47:08 +02:00
fulfillment_rate: Optional[Decimal] = None
on_time_delivery: Optional[bool] = None
quality_rating: Optional[Decimal] = None
2025-10-27 16:33:26 +01:00
2025-08-23 19:47:08 +02:00
approved_quantity: Optional[Decimal] = None
approved_cost: Optional[Decimal] = None
approved_at: Optional[datetime] = None
approved_by: Optional[uuid.UUID] = None
2025-10-27 16:33:26 +01:00
2025-08-23 19:47:08 +02:00
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
2025-10-27 16:33:26 +01:00
# 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
2025-10-30 21:08:07 +01:00
# 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
2025-08-23 19:47:08 +02:00
# ================================================================
# 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)
2025-10-19 19:22:37 +02:00
2025-09-22 16:10:08 +02:00
plan_type: str = Field(default="regular", pattern="^(regular|emergency|seasonal|urgent)$")
2025-10-19 19:22:37 +02:00
priority: str = Field(default="normal", pattern="^(critical|high|normal|low)$")
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
business_model: Optional[str] = Field(None, pattern="^(individual_bakery|central_bakery)$")
2025-09-22 16:10:08 +02:00
procurement_strategy: str = Field(default="just_in_time", pattern="^(just_in_time|bulk|mixed|bulk_order)$")
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
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"))
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02: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)$")
2025-10-19 19:22:37 +02:00
priority: Optional[str] = Field(None, pattern="^(critical|high|normal|low)$")
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
approved_at: Optional[datetime] = None
approved_by: Optional[uuid.UUID] = None
execution_started_at: Optional[datetime] = None
execution_completed_at: Optional[datetime] = None
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
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
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
total_requirements: int
total_estimated_cost: Decimal
total_approved_cost: Decimal
cost_variance: Decimal
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
total_demand_orders: int
total_demand_quantity: Decimal
total_production_requirements: Decimal
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
primary_suppliers_count: int
backup_suppliers_count: int
supplier_diversification_score: Optional[Decimal] = None
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
approved_at: Optional[datetime] = None
approved_by: Optional[uuid.UUID] = None
execution_started_at: Optional[datetime] = None
execution_completed_at: Optional[datetime] = None
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
fulfillment_rate: Optional[Decimal] = None
on_time_delivery_rate: Optional[Decimal] = None
cost_accuracy: Optional[Decimal] = None
quality_score: Optional[Decimal] = None
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
created_at: datetime
updated_at: datetime
created_by: Optional[uuid.UUID] = None
updated_by: Optional[uuid.UUID] = None
2025-10-30 21:08:07 +01:00
# NEW: Track forecast and production schedule links
forecast_id: Optional[uuid.UUID] = None
production_schedule_id: Optional[uuid.UUID] = None
2025-08-23 19:47:08 +02:00
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
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
total_estimated_cost: Decimal
total_approved_cost: Decimal
cost_variance: Decimal
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
average_fulfillment_rate: Optional[Decimal] = None
average_on_time_delivery: Optional[Decimal] = None
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
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
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
upcoming_deliveries: List[Dict[str, Any]] = []
overdue_requirements: List[Dict[str, Any]] = []
low_stock_alerts: List[Dict[str, Any]] = []
2025-10-30 21:08:07 +01:00
2025-08-23 19:47:08 +02:00
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)
2025-10-30 21:08:07 +01:00
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")
2025-08-23 19:47:08 +02:00
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] = []
2025-10-30 21:08:07 +01:00
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]] = []
2025-08-23 19:47:08 +02:00
class PaginatedProcurementPlans(ProcurementBase):
"""Paginated list of procurement plans"""
plans: List[ProcurementPlanResponse]
total: int
page: int
limit: int
2025-10-30 21:08:07 +01:00
has_more: bool