Files
bakery-ia/services/procurement/app/models/replenishment.py
2025-10-30 21:08:07 +01:00

195 lines
7.5 KiB
Python

"""
Database models for replenishment planning.
"""
from sqlalchemy import Column, String, Integer, Numeric, Date, Boolean, ForeignKey, Text, TIMESTAMP, JSON
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
import uuid
from datetime import datetime
from shared.database import Base
class ReplenishmentPlan(Base):
"""Replenishment plan master record"""
__tablename__ = "replenishment_plans"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Planning metadata
planning_date = Column(Date, nullable=False)
projection_horizon_days = Column(Integer, nullable=False, default=7)
# References
forecast_id = Column(UUID(as_uuid=True), nullable=True)
production_schedule_id = Column(UUID(as_uuid=True), nullable=True)
# Summary statistics
total_items = Column(Integer, nullable=False, default=0)
urgent_items = Column(Integer, nullable=False, default=0)
high_risk_items = Column(Integer, nullable=False, default=0)
total_estimated_cost = Column(Numeric(12, 2), nullable=False, default=0)
# Status
status = Column(String(50), nullable=False, default='draft') # draft, approved, executed
# Timestamps
created_at = Column(TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow)
updated_at = Column(TIMESTAMP(timezone=True), nullable=True, onupdate=datetime.utcnow)
executed_at = Column(TIMESTAMP(timezone=True), nullable=True)
# Relationships
items = relationship("ReplenishmentPlanItem", back_populates="plan", cascade="all, delete-orphan")
class ReplenishmentPlanItem(Base):
"""Individual item in a replenishment plan"""
__tablename__ = "replenishment_plan_items"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
replenishment_plan_id = Column(UUID(as_uuid=True), ForeignKey("replenishment_plans.id"), nullable=False, index=True)
# Ingredient info
ingredient_id = Column(UUID(as_uuid=True), nullable=False, index=True)
ingredient_name = Column(String(200), nullable=False)
unit_of_measure = Column(String(20), nullable=False)
# Quantities
base_quantity = Column(Numeric(12, 3), nullable=False)
safety_stock_quantity = Column(Numeric(12, 3), nullable=False, default=0)
shelf_life_adjusted_quantity = Column(Numeric(12, 3), nullable=False)
final_order_quantity = Column(Numeric(12, 3), nullable=False)
# Dates
order_date = Column(Date, nullable=False, index=True)
delivery_date = Column(Date, nullable=False)
required_by_date = Column(Date, nullable=False)
# Planning metadata
lead_time_days = Column(Integer, nullable=False)
is_urgent = Column(Boolean, nullable=False, default=False, index=True)
urgency_reason = Column(Text, nullable=True)
waste_risk = Column(String(20), nullable=False, default='low') # low, medium, high
stockout_risk = Column(String(20), nullable=False, default='low') # low, medium, high, critical
# Supplier
supplier_id = Column(UUID(as_uuid=True), nullable=True)
# Calculation details (stored as JSONB)
safety_stock_calculation = Column(JSONB, nullable=True)
shelf_life_adjustment = Column(JSONB, nullable=True)
inventory_projection = Column(JSONB, nullable=True)
# Timestamps
created_at = Column(TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow)
# Relationships
plan = relationship("ReplenishmentPlan", back_populates="items")
class InventoryProjection(Base):
"""Daily inventory projection"""
__tablename__ = "inventory_projections"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Ingredient
ingredient_id = Column(UUID(as_uuid=True), nullable=False, index=True)
ingredient_name = Column(String(200), nullable=False)
# Projection date
projection_date = Column(Date, nullable=False, index=True)
# Stock levels
starting_stock = Column(Numeric(12, 3), nullable=False)
forecasted_consumption = Column(Numeric(12, 3), nullable=False, default=0)
scheduled_receipts = Column(Numeric(12, 3), nullable=False, default=0)
projected_ending_stock = Column(Numeric(12, 3), nullable=False)
# Flags
is_stockout = Column(Boolean, nullable=False, default=False, index=True)
coverage_gap = Column(Numeric(12, 3), nullable=False, default=0) # Negative if stockout
# Reference to replenishment plan
replenishment_plan_id = Column(UUID(as_uuid=True), nullable=True)
# Timestamps
created_at = Column(TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow)
__table_args__ = (
# Unique constraint: one projection per ingredient per date per tenant
{'schema': None}
)
class SupplierAllocation(Base):
"""Supplier allocation for a requirement"""
__tablename__ = "supplier_allocations"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
# References
replenishment_plan_item_id = Column(UUID(as_uuid=True), ForeignKey("replenishment_plan_items.id"), nullable=True, index=True)
requirement_id = Column(UUID(as_uuid=True), nullable=True, index=True) # Reference to procurement_requirements
# Supplier
supplier_id = Column(UUID(as_uuid=True), nullable=False, index=True)
supplier_name = Column(String(200), nullable=False)
# Allocation
allocation_type = Column(String(20), nullable=False) # primary, backup, diversification
allocated_quantity = Column(Numeric(12, 3), nullable=False)
allocation_percentage = Column(Numeric(5, 4), nullable=False) # 0.0000 - 1.0000
# Pricing
unit_price = Column(Numeric(12, 2), nullable=False)
total_cost = Column(Numeric(12, 2), nullable=False)
# Lead time
lead_time_days = Column(Integer, nullable=False)
# Scoring
supplier_score = Column(Numeric(5, 2), nullable=False)
score_breakdown = Column(JSONB, nullable=True)
# Reasoning
allocation_reason = Column(Text, nullable=True)
# Timestamps
created_at = Column(TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow)
class SupplierSelectionHistory(Base):
"""Historical record of supplier selections for analytics"""
__tablename__ = "supplier_selection_history"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Selection details
ingredient_id = Column(UUID(as_uuid=True), nullable=False, index=True)
ingredient_name = Column(String(200), nullable=False)
selected_supplier_id = Column(UUID(as_uuid=True), nullable=False, index=True)
selected_supplier_name = Column(String(200), nullable=False)
# Order details
selection_date = Column(Date, nullable=False, index=True)
quantity = Column(Numeric(12, 3), nullable=False)
unit_price = Column(Numeric(12, 2), nullable=False)
total_cost = Column(Numeric(12, 2), nullable=False)
# Metrics
lead_time_days = Column(Integer, nullable=False)
quality_score = Column(Numeric(5, 2), nullable=True)
delivery_performance = Column(Numeric(5, 2), nullable=True)
# Selection strategy
selection_strategy = Column(String(50), nullable=False) # single_source, dual_source, multi_source
was_primary_choice = Column(Boolean, nullable=False, default=True)
# Timestamps
created_at = Column(TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow)