Files
bakery-ia/services/distribution/app/models/distribution.py

180 lines
7.8 KiB
Python
Raw Normal View History

2025-11-30 09:12:40 +01:00
"""
Distribution models for the bakery management platform
"""
import uuid
import enum
from datetime import datetime, timezone
from decimal import Decimal
from sqlalchemy import Column, String, DateTime, Float, Integer, Text, Index, Boolean, Numeric, ForeignKey, Enum as SQLEnum
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from shared.database.base import Base
class DeliveryRouteStatus(enum.Enum):
"""Status of delivery routes"""
planned = "planned"
in_progress = "in_progress"
completed = "completed"
cancelled = "cancelled"
class ShipmentStatus(enum.Enum):
"""Status of individual shipments"""
pending = "pending"
packed = "packed"
in_transit = "in_transit"
delivered = "delivered"
failed = "failed"
class DeliveryScheduleFrequency(enum.Enum):
"""Frequency of recurring delivery schedules"""
daily = "daily"
weekly = "weekly"
biweekly = "biweekly"
monthly = "monthly"
class DeliveryRoute(Base):
"""Optimized multi-stop routes for distribution"""
__tablename__ = "delivery_routes"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Route identification
route_number = Column(String(50), nullable=False, unique=True, index=True)
route_date = Column(DateTime(timezone=True), nullable=False, index=True) # Date when route is executed
# Vehicle and driver assignment
vehicle_id = Column(String(100), nullable=True) # Reference to fleet management
driver_id = Column(UUID(as_uuid=True), nullable=True, index=True) # Reference to driver
# Optimization metadata
total_distance_km = Column(Float, nullable=True)
estimated_duration_minutes = Column(Integer, nullable=True)
2025-12-17 20:50:22 +01:00
# VRP Optimization metrics (Phase 2 enhancement)
vrp_optimization_savings = Column(JSONB, nullable=True) # {"distance_saved_km": 12.5, "time_saved_minutes": 25, "fuel_saved_liters": 8.2, "co2_saved_kg": 15.4, "cost_saved_eur": 12.50}
vrp_algorithm_version = Column(String(50), nullable=True) # Version of VRP algorithm used
vrp_optimization_timestamp = Column(DateTime(timezone=True), nullable=True) # When optimization was performed
vrp_constraints_satisfied = Column(Boolean, nullable=True) # Whether all constraints were satisfied
vrp_objective_value = Column(Float, nullable=True) # Objective function value from optimization
2025-11-30 09:12:40 +01:00
# Route details
route_sequence = Column(JSONB, nullable=True) # Ordered array of stops with timing: [{"stop_number": 1, "location_id": "...", "estimated_arrival": "...", "actual_arrival": "..."}]
notes = Column(Text, nullable=True)
# Status
status = Column(SQLEnum(DeliveryRouteStatus), nullable=False, default=DeliveryRouteStatus.planned, index=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=False)
updated_by = Column(UUID(as_uuid=True), nullable=False)
# Relationships
shipments = relationship("Shipment", back_populates="route", cascade="all, delete-orphan")
# Indexes
__table_args__ = (
Index('ix_delivery_routes_tenant_date', 'tenant_id', 'route_date'),
Index('ix_delivery_routes_status', 'status'),
Index('ix_delivery_routes_date_tenant_status', 'route_date', 'tenant_id', 'status'),
)
class Shipment(Base):
"""Individual deliveries to child tenants"""
__tablename__ = "shipments"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Links to hierarchy and procurement
parent_tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Source tenant (central production)
child_tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Destination tenant (retail outlet)
purchase_order_id = Column(UUID(as_uuid=True), nullable=True, index=True) # Associated internal purchase order
delivery_route_id = Column(UUID(as_uuid=True), ForeignKey('delivery_routes.id', ondelete='SET NULL'), nullable=True, index=True) # Assigned route
# Shipment details
shipment_number = Column(String(50), nullable=False, unique=True, index=True)
shipment_date = Column(DateTime(timezone=True), nullable=False, index=True)
# Tracking information
current_location_lat = Column(Float, nullable=True)
current_location_lng = Column(Float, nullable=True)
last_tracked_at = Column(DateTime(timezone=True), nullable=True)
status = Column(SQLEnum(ShipmentStatus), nullable=False, default=ShipmentStatus.pending, index=True)
actual_delivery_time = Column(DateTime(timezone=True), nullable=True)
# Proof of delivery
signature = Column(Text, nullable=True) # Digital signature base64 encoded
photo_url = Column(String(500), nullable=True) # URL to delivery confirmation photo
received_by_name = Column(String(200), nullable=True)
delivery_notes = Column(Text, nullable=True)
# Weight/volume tracking
total_weight_kg = Column(Float, nullable=True)
total_volume_m3 = Column(Float, 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=False)
updated_by = Column(UUID(as_uuid=True), nullable=False)
# Relationships
route = relationship("DeliveryRoute", back_populates="shipments")
# Indexes
__table_args__ = (
Index('ix_shipments_tenant_status', 'tenant_id', 'status'),
Index('ix_shipments_parent_child', 'parent_tenant_id', 'child_tenant_id'),
Index('ix_shipments_date_tenant', 'shipment_date', 'tenant_id'),
)
class DeliverySchedule(Base):
"""Recurring delivery patterns"""
__tablename__ = "delivery_schedules"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Schedule identification
name = Column(String(200), nullable=False)
# Delivery pattern
delivery_days = Column(String(200), nullable=False) # Format: "Mon,Wed,Fri" or "Mon-Fri"
delivery_time = Column(String(20), nullable=False) # Format: "HH:MM" or "HH:MM-HH:MM"
frequency = Column(SQLEnum(DeliveryScheduleFrequency), nullable=False, default=DeliveryScheduleFrequency.weekly)
# Auto-generation settings
auto_generate_orders = Column(Boolean, nullable=False, default=False)
lead_time_days = Column(Integer, nullable=False, default=1) # How many days in advance to generate
# Target tenants for this schedule
target_parent_tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
target_child_tenant_ids = Column(JSONB, nullable=False) # List of child tenant IDs involved in this route
# Configuration
is_active = Column(Boolean, nullable=False, default=True)
notes = Column(Text, 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=False)
updated_by = Column(UUID(as_uuid=True), nullable=False)
# Indexes
__table_args__ = (
Index('ix_delivery_schedules_tenant_active', 'tenant_id', 'is_active'),
Index('ix_delivery_schedules_parent_tenant', 'target_parent_tenant_id'),
)