2025-08-21 20:28:14 +02:00
|
|
|
# ================================================================
|
|
|
|
|
# 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
|
|
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
# 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
|
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)
|
|
|
|
|
|
|
|
|
|
# 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")
|