Files
bakery-ia/services/procurement/app/models/procurement_plan.py

235 lines
12 KiB
Python
Raw Normal View History

2025-08-21 20:28:14 +02:00
# ================================================================
2025-10-30 21:08:07 +01:00
# services/procurement/app/models/procurement_plan.py
2025-08-21 20:28:14 +02:00
# ================================================================
"""
2025-10-30 21:08:07 +01:00
Procurement Planning Models
Migrated from Orders Service
2025-08-21 20:28:14 +02:00
"""
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
2025-10-01 11:24:06 +02:00
from shared.database.base import Base
2025-08-21 20:28:14 +02:00
class ProcurementPlan(Base):
"""Master procurement plan for coordinating supply needs across orders and production"""
__tablename__ = "procurement_plans"
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# Plan status and lifecycle
status = Column(String(50), nullable=False, default="draft", index=True)
# Status values: draft, pending_approval, approved, in_execution, completed, cancelled
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
plan_type = Column(String(50), nullable=False, default="regular") # regular, emergency, seasonal
priority = Column(String(20), nullable=False, default="normal") # high, normal, low
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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"))
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02: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
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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"))
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02: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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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
2025-10-30 21:08:07 +01:00
forecast_data = Column(JSONB, nullable=True) # Forecasting service data used for this plan
2025-08-21 20:28:14 +02:00
# Communication and collaboration
stakeholder_notifications = Column(JSONB, nullable=True) # Who was notified and when
approval_workflow = Column(JSONB, nullable=True) # Approval chain and status
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# Special considerations
special_requirements = Column(Text, nullable=True)
seasonal_adjustments = Column(JSONB, nullable=True)
emergency_provisions = Column(JSONB, nullable=True)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# External references
erp_reference = Column(String(100), nullable=True)
supplier_portal_reference = Column(String(100), nullable=True)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# Additional metadata
plan_metadata = Column(JSONB, nullable=True)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# Relationships
requirements = relationship("ProcurementRequirement", back_populates="plan", cascade="all, delete-orphan")
class ProcurementRequirement(Base):
"""Individual procurement requirements within a procurement plan"""
__tablename__ = "procurement_requirements"
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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
2025-10-30 21:08:07 +01:00
# 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
2025-08-21 20:28:14 +02:00
# 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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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"))
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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"))
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02: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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# Requirement status
status = Column(String(50), nullable=False, default="pending")
# Status values: pending, approved, ordered, partially_received, received, cancelled
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
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
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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")
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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
2025-10-27 16:33:26 +01:00
# 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
2025-08-21 20:28:14 +02:00
# 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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# Notes and communication
procurement_notes = Column(Text, nullable=True)
supplier_communication = Column(JSONB, nullable=True)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# 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)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# Additional metadata
requirement_metadata = Column(JSONB, nullable=True)
2025-10-30 21:08:07 +01:00
2025-08-21 20:28:14 +02:00
# Relationships
2025-10-30 21:08:07 +01:00
plan = relationship("ProcurementPlan", back_populates="requirements")