# ================================================================ # services/procurement/app/models/procurement_plan.py # ================================================================ """ Procurement Planning Models Migrated from Orders Service """ import uuid from datetime import datetime, date from decimal import Decimal 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 shared.database.base 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 forecast_data = Column(JSONB, nullable=True) # Forecasting service data used for this plan # 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 # Local production tracking is_locally_produced = Column(Boolean, nullable=False, default=False) # If true, this is for a locally-produced item recipe_id = Column(UUID(as_uuid=True), nullable=True) # Recipe used for BOM explosion parent_requirement_id = Column(UUID(as_uuid=True), nullable=True) # If this is from BOM explosion bom_explosion_level = Column(Integer, nullable=False, default=0) # Depth in BOM tree # 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 # Smart procurement calculation metadata calculation_method = Column(String(100), nullable=True) # Method used: REORDER_POINT_TRIGGERED, FORECAST_DRIVEN_PROACTIVE, etc. ai_suggested_quantity = Column(Numeric(12, 3), nullable=True) # Pure AI forecast quantity adjusted_quantity = Column(Numeric(12, 3), nullable=True) # Final quantity after applying constraints adjustment_reason = Column(Text, nullable=True) # Human-readable explanation of adjustments price_tier_applied = Column(JSONB, nullable=True) # Price tier information if applicable supplier_minimum_applied = Column(Boolean, nullable=False, default=False) # Whether supplier minimum was enforced storage_limit_applied = Column(Boolean, nullable=False, default=False) # Whether storage limit was hit reorder_rule_applied = Column(Boolean, nullable=False, default=False) # Whether reorder rules were used # 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")