Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
# ================================================================
# services/procurement/app/schemas/__init__.py
# ================================================================
"""
Pydantic schemas for Procurement Service
"""
from .procurement_schemas import (
ProcurementRequirementBase,
ProcurementRequirementCreate,
ProcurementRequirementUpdate,
ProcurementRequirementResponse,
ProcurementPlanBase,
ProcurementPlanCreate,
ProcurementPlanUpdate,
ProcurementPlanResponse,
ProcurementSummary,
DashboardData,
GeneratePlanRequest,
GeneratePlanResponse,
AutoGenerateProcurementRequest,
AutoGenerateProcurementResponse,
PaginatedProcurementPlans,
)
from .purchase_order_schemas import (
PurchaseOrderCreate,
PurchaseOrderUpdate,
PurchaseOrderApproval,
PurchaseOrderResponse,
PurchaseOrderSummary,
PurchaseOrderItemCreate,
PurchaseOrderItemResponse,
DeliveryCreate,
DeliveryUpdate,
DeliveryResponse,
DeliveryItemCreate,
DeliveryItemResponse,
SupplierInvoiceCreate,
SupplierInvoiceUpdate,
SupplierInvoiceResponse,
)
__all__ = [
# Procurement Plan schemas
"ProcurementRequirementBase",
"ProcurementRequirementCreate",
"ProcurementRequirementUpdate",
"ProcurementRequirementResponse",
"ProcurementPlanBase",
"ProcurementPlanCreate",
"ProcurementPlanUpdate",
"ProcurementPlanResponse",
"ProcurementSummary",
"DashboardData",
"GeneratePlanRequest",
"GeneratePlanResponse",
"AutoGenerateProcurementRequest",
"AutoGenerateProcurementResponse",
"PaginatedProcurementPlans",
# Purchase Order schemas
"PurchaseOrderCreate",
"PurchaseOrderUpdate",
"PurchaseOrderApproval",
"PurchaseOrderResponse",
"PurchaseOrderSummary",
"PurchaseOrderItemCreate",
"PurchaseOrderItemResponse",
# Delivery schemas
"DeliveryCreate",
"DeliveryUpdate",
"DeliveryResponse",
"DeliveryItemCreate",
"DeliveryItemResponse",
# Invoice schemas
"SupplierInvoiceCreate",
"SupplierInvoiceUpdate",
"SupplierInvoiceResponse",
]

View File

@@ -0,0 +1,368 @@
# ================================================================
# 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

View File

@@ -0,0 +1,395 @@
# ================================================================
# services/procurement/app/schemas/purchase_order_schemas.py
# ================================================================
"""
Purchase Order Schemas - Request/response models for purchase orders
Migrated from Suppliers Service with procurement-specific additions
"""
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 PurchaseOrderBase(BaseModel):
"""Base schema for purchase order entities"""
model_config = ConfigDict(from_attributes=True, str_strip_whitespace=True)
# ================================================================
# PURCHASE ORDER ITEM SCHEMAS
# ================================================================
class PurchaseOrderItemCreate(PurchaseOrderBase):
"""Schema for creating purchase order items"""
inventory_product_id: uuid.UUID # Changed from ingredient_id to match model
ordered_quantity: Decimal = Field(..., gt=0)
unit_price: Decimal = Field(..., gt=0)
unit_of_measure: str = Field(..., max_length=50)
quality_requirements: Optional[str] = None
item_notes: Optional[str] = None
class PurchaseOrderItemUpdate(PurchaseOrderBase):
"""Schema for updating purchase order items"""
ordered_quantity: Optional[Decimal] = Field(None, gt=0)
unit_price: Optional[Decimal] = Field(None, gt=0)
quality_requirements: Optional[str] = None
item_notes: Optional[str] = None
class PurchaseOrderItemResponse(PurchaseOrderBase):
"""Schema for purchase order item responses"""
id: uuid.UUID
tenant_id: uuid.UUID
purchase_order_id: uuid.UUID
inventory_product_id: uuid.UUID # Changed from ingredient_id to match model
product_name: Optional[str] = None
ordered_quantity: Decimal
received_quantity: Decimal
unit_price: Decimal
unit_of_measure: str
line_total: Decimal
quality_requirements: Optional[str] = None
item_notes: Optional[str] = None
created_at: datetime
updated_at: datetime
# ================================================================
# PURCHASE ORDER SCHEMAS
# ================================================================
class PurchaseOrderCreate(PurchaseOrderBase):
"""Schema for creating purchase orders"""
supplier_id: uuid.UUID
required_delivery_date: datetime # Use datetime with timezone
priority: str = Field(default="normal", pattern="^(low|normal|high|critical)$")
# Financial information
tax_amount: Decimal = Field(default=Decimal("0"), ge=0)
shipping_cost: Decimal = Field(default=Decimal("0"), ge=0)
discount_amount: Decimal = Field(default=Decimal("0"), ge=0)
subtotal: Decimal = Field(..., ge=0)
# Additional information
notes: Optional[str] = None
# NEW: Procurement-specific fields
procurement_plan_id: Optional[uuid.UUID] = None
# Items
items: List[PurchaseOrderItemCreate] = Field(..., min_length=1)
class PurchaseOrderUpdate(PurchaseOrderBase):
"""Schema for updating purchase orders"""
required_delivery_date: Optional[datetime] = None # Use datetime with timezone
priority: Optional[str] = Field(None, pattern="^(low|normal|high|critical)$")
# Financial information
tax_amount: Optional[Decimal] = Field(None, ge=0)
shipping_cost: Optional[Decimal] = Field(None, ge=0)
discount_amount: Optional[Decimal] = Field(None, ge=0)
# Additional information
notes: Optional[str] = None
class PurchaseOrderApproval(PurchaseOrderBase):
"""Schema for purchase order approval/rejection"""
action: str = Field(..., pattern="^(approve|reject)$")
notes: Optional[str] = None
approved_by: Optional[uuid.UUID] = None
class SupplierSummary(PurchaseOrderBase):
"""Schema for supplier summary - matches the structure returned by suppliers service"""
id: str
name: str
supplier_code: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
contact_person: Optional[str] = None
address_line1: Optional[str] = None
city: Optional[str] = None
country: Optional[str] = None
supplier_type: Optional[str] = None
status: Optional[str] = None
mobile: Optional[str] = None
website: Optional[str] = None
payment_terms: Optional[str] = None
standard_lead_time: Optional[int] = None
quality_rating: Optional[float] = None
delivery_rating: Optional[float] = None
total_orders: Optional[int] = None
total_amount: Optional[float] = None
class PurchaseOrderResponse(PurchaseOrderBase):
"""Schema for purchase order responses"""
id: uuid.UUID
tenant_id: uuid.UUID
supplier_id: uuid.UUID
supplier_name: Optional[str] = None
po_number: str
status: str
priority: str
order_date: datetime
required_delivery_date: Optional[datetime] = None # Use datetime with timezone
estimated_delivery_date: Optional[datetime] = None # Use datetime with timezone
actual_delivery_date: Optional[datetime] = None # Use datetime with timezone
# Financial information
subtotal: Decimal
tax_amount: Decimal
shipping_cost: Decimal
discount_amount: Decimal
total_amount: Decimal
currency: str
# Approval workflow
approved_by: Optional[uuid.UUID] = None
approved_at: Optional[datetime] = None
rejection_reason: Optional[str] = None
# NEW: Procurement-specific fields
procurement_plan_id: Optional[uuid.UUID] = None
auto_approved: bool = False
auto_approval_rule_id: Optional[uuid.UUID] = None
# Additional information
notes: Optional[str] = None
# AI/ML reasoning for procurement decisions (JTBD dashboard support)
reasoning_data: Optional[Dict[str, Any]] = None
# Audit fields
created_at: datetime
updated_at: datetime
created_by: Optional[uuid.UUID] = None
updated_by: Optional[uuid.UUID] = None
# Related data
items: List[PurchaseOrderItemResponse] = []
class PurchaseOrderWithSupplierResponse(PurchaseOrderResponse):
"""Schema for purchase order responses with supplier information"""
supplier: Optional[SupplierSummary] = None
class PurchaseOrderSummary(PurchaseOrderBase):
"""Schema for purchase order summary (list view)"""
id: uuid.UUID
po_number: str
supplier_id: uuid.UUID
supplier_name: Optional[str] = None
status: str
priority: str
order_date: datetime
required_delivery_date: datetime # Use datetime with timezone
total_amount: Decimal
currency: str
auto_approved: bool = False
created_at: datetime
# ================================================================
# DELIVERY SCHEMAS
# ================================================================
class DeliveryItemCreate(PurchaseOrderBase):
"""Schema for creating delivery items"""
purchase_order_item_id: uuid.UUID
inventory_product_id: uuid.UUID # Changed from ingredient_id to match model
ordered_quantity: Decimal = Field(..., gt=0)
delivered_quantity: Decimal = Field(..., ge=0)
accepted_quantity: Decimal = Field(..., ge=0)
rejected_quantity: Decimal = Field(default=Decimal("0"), ge=0)
# Quality information
batch_lot_number: Optional[str] = Field(None, max_length=100)
expiry_date: Optional[datetime] = None # Use datetime with timezone
quality_grade: Optional[str] = Field(None, max_length=20)
# Issues and notes
quality_issues: Optional[str] = None
rejection_reason: Optional[str] = None
item_notes: Optional[str] = None
class DeliveryItemResponse(PurchaseOrderBase):
"""Schema for delivery item responses"""
id: uuid.UUID
tenant_id: uuid.UUID
delivery_id: uuid.UUID
purchase_order_item_id: uuid.UUID
inventory_product_id: uuid.UUID # Changed from ingredient_id to match model
ingredient_name: Optional[str] = None
ordered_quantity: Decimal
delivered_quantity: Decimal
accepted_quantity: Decimal
rejected_quantity: Decimal
batch_lot_number: Optional[str] = None
expiry_date: Optional[datetime] = None # Use datetime with timezone
quality_grade: Optional[str] = None
quality_issues: Optional[str] = None
rejection_reason: Optional[str] = None
item_notes: Optional[str] = None
created_at: datetime
updated_at: datetime
class DeliveryCreate(PurchaseOrderBase):
"""Schema for creating deliveries"""
purchase_order_id: uuid.UUID
supplier_id: uuid.UUID
supplier_delivery_note: Optional[str] = Field(None, max_length=100)
scheduled_date: Optional[datetime] = None # Use datetime with timezone
estimated_arrival: Optional[datetime] = None
# Delivery details
carrier_name: Optional[str] = Field(None, max_length=200)
tracking_number: Optional[str] = Field(None, max_length=100)
# Additional information
notes: Optional[str] = None
# Items
items: List[DeliveryItemCreate] = Field(..., min_length=1)
class DeliveryUpdate(PurchaseOrderBase):
"""Schema for updating deliveries"""
supplier_delivery_note: Optional[str] = Field(None, max_length=100)
scheduled_date: Optional[datetime] = None # Use datetime with timezone
estimated_arrival: Optional[datetime] = None
actual_arrival: Optional[datetime] = None
# Delivery details
carrier_name: Optional[str] = Field(None, max_length=200)
tracking_number: Optional[str] = Field(None, max_length=100)
# Quality inspection
inspection_passed: Optional[bool] = None
inspection_notes: Optional[str] = None
quality_issues: Optional[Dict[str, Any]] = None
# Additional information
notes: Optional[str] = None
class DeliveryResponse(PurchaseOrderBase):
"""Schema for delivery responses"""
id: uuid.UUID
tenant_id: uuid.UUID
purchase_order_id: uuid.UUID
supplier_id: uuid.UUID
supplier_name: Optional[str] = None
delivery_number: str
supplier_delivery_note: Optional[str] = None
status: str
# Timing
scheduled_date: Optional[datetime] = None # Use datetime with timezone
estimated_arrival: Optional[datetime] = None
actual_arrival: Optional[datetime] = None
completed_at: Optional[datetime] = None
# Delivery details
carrier_name: Optional[str] = None
tracking_number: Optional[str] = None
# Quality inspection
inspection_passed: Optional[bool] = None
inspection_notes: Optional[str] = None
quality_issues: Optional[Dict[str, Any]] = None
# Receipt information
received_by: Optional[uuid.UUID] = None
received_at: Optional[datetime] = None
# Additional information
notes: Optional[str] = None
# Audit fields
created_at: datetime
updated_at: datetime
created_by: uuid.UUID
# Related data
items: List[DeliveryItemResponse] = []
# ================================================================
# INVOICE SCHEMAS
# ================================================================
class SupplierInvoiceCreate(PurchaseOrderBase):
"""Schema for creating supplier invoices"""
purchase_order_id: uuid.UUID
supplier_id: uuid.UUID
invoice_number: str = Field(..., max_length=100)
invoice_date: datetime # Use datetime with timezone
due_date: datetime # Use datetime with timezone
# Financial information
subtotal: Decimal = Field(..., ge=0)
tax_amount: Decimal = Field(default=Decimal("0"), ge=0)
shipping_cost: Decimal = Field(default=Decimal("0"), ge=0)
discount_amount: Decimal = Field(default=Decimal("0"), ge=0)
# Additional information
notes: Optional[str] = None
payment_reference: Optional[str] = Field(None, max_length=100)
class SupplierInvoiceUpdate(PurchaseOrderBase):
"""Schema for updating supplier invoices"""
due_date: Optional[datetime] = None # Use datetime with timezone
payment_reference: Optional[str] = Field(None, max_length=100)
notes: Optional[str] = None
class SupplierInvoiceResponse(PurchaseOrderBase):
"""Schema for supplier invoice responses"""
id: uuid.UUID
tenant_id: uuid.UUID
purchase_order_id: uuid.UUID
supplier_id: uuid.UUID
supplier_name: Optional[str] = None
invoice_number: str
status: str
invoice_date: datetime # Use datetime with timezone
due_date: datetime # Use datetime with timezone
# Financial information
subtotal: Decimal
tax_amount: Decimal
shipping_cost: Decimal
discount_amount: Decimal
total_amount: Decimal
currency: str
# Payment tracking
paid_amount: Decimal
remaining_amount: Decimal
payment_date: Optional[datetime] = None # Use datetime with timezone
payment_reference: Optional[str] = None
# Additional information
notes: Optional[str] = None
# Audit fields
created_at: datetime
updated_at: datetime
created_by: uuid.UUID
updated_by: uuid.UUID

View File

@@ -0,0 +1,440 @@
"""
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