# ================================================================ # services/orders/app/models/procurement.py # ================================================================ """ Procurement planning database models for Orders Service """ import uuid from datetime import datetime, date from decimal import Decimal from typing import Optional, List from sqlalchemy import Column, String, Boolean, DateTime, Date, Numeric, Text, Integer, ForeignKey from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.core.database import Base class ProcurementPlan(Base): """Master procurement plan for coordinating supply needs across orders and production""" __tablename__ = "procurement_plans" # Primary identification id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True) plan_number = Column(String(50), nullable=False, unique=True, index=True) # Plan scope and timing plan_date = Column(Date, nullable=False, index=True) plan_period_start = Column(Date, nullable=False) plan_period_end = Column(Date, nullable=False) planning_horizon_days = Column(Integer, nullable=False, default=14) # Plan status and lifecycle status = Column(String(50), nullable=False, default="draft", index=True) # Status values: draft, pending_approval, approved, in_execution, completed, cancelled plan_type = Column(String(50), nullable=False, default="regular") # regular, emergency, seasonal priority = Column(String(20), nullable=False, default="normal") # high, normal, low # Business model context business_model = Column(String(50), nullable=True) # individual_bakery, central_bakery procurement_strategy = Column(String(50), nullable=False, default="just_in_time") # just_in_time, bulk, mixed # Plan totals and summary total_requirements = Column(Integer, nullable=False, default=0) total_estimated_cost = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00")) total_approved_cost = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00")) cost_variance = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00")) # Demand analysis total_demand_orders = Column(Integer, nullable=False, default=0) total_demand_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) total_production_requirements = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) safety_stock_buffer = Column(Numeric(5, 2), nullable=False, default=Decimal("20.00")) # Percentage # Supplier coordination primary_suppliers_count = Column(Integer, nullable=False, default=0) backup_suppliers_count = Column(Integer, nullable=False, default=0) supplier_diversification_score = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0 # Risk assessment supply_risk_level = Column(String(20), nullable=False, default="low") # low, medium, high, critical demand_forecast_confidence = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0 seasonality_adjustment = Column(Numeric(5, 2), nullable=False, default=Decimal("0.00")) # Execution tracking approved_at = Column(DateTime(timezone=True), nullable=True) approved_by = Column(UUID(as_uuid=True), nullable=True) execution_started_at = Column(DateTime(timezone=True), nullable=True) execution_completed_at = Column(DateTime(timezone=True), nullable=True) # Performance metrics fulfillment_rate = Column(Numeric(5, 2), nullable=True) # Percentage on_time_delivery_rate = Column(Numeric(5, 2), nullable=True) # Percentage cost_accuracy = Column(Numeric(5, 2), nullable=True) # Percentage quality_score = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0 # Integration data source_orders = Column(JSONB, nullable=True) # Orders that drove this plan production_schedules = Column(JSONB, nullable=True) # Associated production schedules inventory_snapshots = Column(JSONB, nullable=True) # Inventory levels at planning time # Communication and collaboration stakeholder_notifications = Column(JSONB, nullable=True) # Who was notified and when approval_workflow = Column(JSONB, nullable=True) # Approval chain and status # Special considerations special_requirements = Column(Text, nullable=True) seasonal_adjustments = Column(JSONB, nullable=True) emergency_provisions = Column(JSONB, nullable=True) # External references erp_reference = Column(String(100), nullable=True) supplier_portal_reference = Column(String(100), nullable=True) # Audit fields created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False) created_by = Column(UUID(as_uuid=True), nullable=True) updated_by = Column(UUID(as_uuid=True), nullable=True) # Additional metadata plan_metadata = Column(JSONB, nullable=True) # Relationships requirements = relationship("ProcurementRequirement", back_populates="plan", cascade="all, delete-orphan") class ProcurementRequirement(Base): """Individual procurement requirements within a procurement plan""" __tablename__ = "procurement_requirements" # Primary identification id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) plan_id = Column(UUID(as_uuid=True), ForeignKey("procurement_plans.id", ondelete="CASCADE"), nullable=False) requirement_number = Column(String(50), nullable=False, index=True) # Product/ingredient information product_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Reference to products/ingredients product_name = Column(String(200), nullable=False) product_sku = Column(String(100), nullable=True) product_category = Column(String(100), nullable=True) product_type = Column(String(50), nullable=False, default="ingredient") # ingredient, packaging, supplies # Requirement details required_quantity = Column(Numeric(12, 3), nullable=False) unit_of_measure = Column(String(50), nullable=False) safety_stock_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) total_quantity_needed = Column(Numeric(12, 3), nullable=False) # Current inventory situation current_stock_level = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) reserved_stock = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) available_stock = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) net_requirement = Column(Numeric(12, 3), nullable=False) # Demand breakdown order_demand = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) production_demand = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) forecast_demand = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) buffer_demand = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) # Supplier information preferred_supplier_id = Column(UUID(as_uuid=True), nullable=True) backup_supplier_id = Column(UUID(as_uuid=True), nullable=True) supplier_name = Column(String(200), nullable=True) supplier_lead_time_days = Column(Integer, nullable=True) minimum_order_quantity = Column(Numeric(12, 3), nullable=True) # Pricing and cost estimated_unit_cost = Column(Numeric(10, 4), nullable=True) estimated_total_cost = Column(Numeric(12, 2), nullable=True) last_purchase_cost = Column(Numeric(10, 4), nullable=True) cost_variance = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00")) # Timing requirements required_by_date = Column(Date, nullable=False) lead_time_buffer_days = Column(Integer, nullable=False, default=1) suggested_order_date = Column(Date, nullable=False) latest_order_date = Column(Date, nullable=False) # Quality and specifications quality_specifications = Column(JSONB, nullable=True) special_requirements = Column(Text, nullable=True) storage_requirements = Column(String(200), nullable=True) shelf_life_days = Column(Integer, nullable=True) # Requirement status status = Column(String(50), nullable=False, default="pending") # Status values: pending, approved, ordered, partially_received, received, cancelled priority = Column(String(20), nullable=False, default="normal") # critical, high, normal, low risk_level = Column(String(20), nullable=False, default="low") # low, medium, high, critical # Purchase order tracking purchase_order_id = Column(UUID(as_uuid=True), nullable=True) purchase_order_number = Column(String(50), nullable=True) ordered_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) ordered_at = Column(DateTime(timezone=True), nullable=True) # Delivery tracking expected_delivery_date = Column(Date, nullable=True) actual_delivery_date = Column(Date, nullable=True) received_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) delivery_status = Column(String(50), nullable=False, default="pending") # Performance tracking fulfillment_rate = Column(Numeric(5, 2), nullable=True) # Percentage on_time_delivery = Column(Boolean, nullable=True) quality_rating = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0 # Source traceability source_orders = Column(JSONB, nullable=True) # Orders that contributed to this requirement source_production_batches = Column(JSONB, nullable=True) # Production batches needing this demand_analysis = Column(JSONB, nullable=True) # Detailed demand breakdown # Approval and authorization approved_quantity = Column(Numeric(12, 3), nullable=True) approved_cost = Column(Numeric(12, 2), nullable=True) approved_at = Column(DateTime(timezone=True), nullable=True) approved_by = Column(UUID(as_uuid=True), nullable=True) # Notes and communication procurement_notes = Column(Text, nullable=True) supplier_communication = Column(JSONB, nullable=True) # Audit fields created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False) # Additional metadata requirement_metadata = Column(JSONB, nullable=True) # Relationships plan = relationship("ProcurementPlan", back_populates="requirements")