# ================================================================ # services/production/app/models/production.py # ================================================================ """ Production models for the production service """ from sqlalchemy import Column, String, Integer, Float, DateTime, Boolean, Text, JSON, Enum as SQLEnum from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.sql import func from datetime import datetime, timezone from typing import Dict, Any, Optional import uuid import enum from shared.database.base import Base class ProductionStatus(str, enum.Enum): """Production batch status enumeration""" PENDING = "pending" IN_PROGRESS = "in_progress" COMPLETED = "completed" CANCELLED = "cancelled" ON_HOLD = "on_hold" QUALITY_CHECK = "quality_check" FAILED = "failed" class ProductionPriority(str, enum.Enum): """Production priority levels""" LOW = "low" MEDIUM = "medium" HIGH = "high" URGENT = "urgent" class ProductionBatch(Base): """Production batch model for tracking individual production runs""" __tablename__ = "production_batches" # 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) batch_number = Column(String(50), nullable=False, unique=True, index=True) # Product and recipe information product_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Reference to inventory/recipes product_name = Column(String(255), nullable=False) recipe_id = Column(UUID(as_uuid=True), nullable=True) # Production planning planned_start_time = Column(DateTime(timezone=True), nullable=False) planned_end_time = Column(DateTime(timezone=True), nullable=False) planned_quantity = Column(Float, nullable=False) planned_duration_minutes = Column(Integer, nullable=False) # Actual production tracking actual_start_time = Column(DateTime(timezone=True), nullable=True) actual_end_time = Column(DateTime(timezone=True), nullable=True) actual_quantity = Column(Float, nullable=True) actual_duration_minutes = Column(Integer, nullable=True) # Status and priority status = Column(SQLEnum(ProductionStatus), nullable=False, default=ProductionStatus.PENDING, index=True) priority = Column(SQLEnum(ProductionPriority), nullable=False, default=ProductionPriority.MEDIUM) # Cost tracking estimated_cost = Column(Float, nullable=True) actual_cost = Column(Float, nullable=True) labor_cost = Column(Float, nullable=True) material_cost = Column(Float, nullable=True) overhead_cost = Column(Float, nullable=True) # Quality metrics yield_percentage = Column(Float, nullable=True) # actual/planned quantity quality_score = Column(Float, nullable=True) waste_quantity = Column(Float, nullable=True) defect_quantity = Column(Float, nullable=True) # Equipment and resources equipment_used = Column(JSON, nullable=True) # List of equipment IDs staff_assigned = Column(JSON, nullable=True) # List of staff IDs station_id = Column(String(50), nullable=True) # Business context order_id = Column(UUID(as_uuid=True), nullable=True) # Associated customer order forecast_id = Column(UUID(as_uuid=True), nullable=True) # Associated demand forecast is_rush_order = Column(Boolean, default=False) is_special_recipe = Column(Boolean, default=False) # Notes and tracking production_notes = Column(Text, nullable=True) quality_notes = Column(Text, nullable=True) delay_reason = Column(String(255), nullable=True) cancellation_reason = Column(String(255), nullable=True) # Timestamps created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) completed_at = Column(DateTime(timezone=True), nullable=True) def to_dict(self) -> Dict[str, Any]: """Convert to dictionary following shared pattern""" return { "id": str(self.id), "tenant_id": str(self.tenant_id), "batch_number": self.batch_number, "product_id": str(self.product_id), "product_name": self.product_name, "recipe_id": str(self.recipe_id) if self.recipe_id else None, "planned_start_time": self.planned_start_time.isoformat() if self.planned_start_time else None, "planned_end_time": self.planned_end_time.isoformat() if self.planned_end_time else None, "planned_quantity": self.planned_quantity, "planned_duration_minutes": self.planned_duration_minutes, "actual_start_time": self.actual_start_time.isoformat() if self.actual_start_time else None, "actual_end_time": self.actual_end_time.isoformat() if self.actual_end_time else None, "actual_quantity": self.actual_quantity, "actual_duration_minutes": self.actual_duration_minutes, "status": self.status.value if self.status else None, "priority": self.priority.value if self.priority else None, "estimated_cost": self.estimated_cost, "actual_cost": self.actual_cost, "labor_cost": self.labor_cost, "material_cost": self.material_cost, "overhead_cost": self.overhead_cost, "yield_percentage": self.yield_percentage, "quality_score": self.quality_score, "waste_quantity": self.waste_quantity, "defect_quantity": self.defect_quantity, "equipment_used": self.equipment_used, "staff_assigned": self.staff_assigned, "station_id": self.station_id, "order_id": str(self.order_id) if self.order_id else None, "forecast_id": str(self.forecast_id) if self.forecast_id else None, "is_rush_order": self.is_rush_order, "is_special_recipe": self.is_special_recipe, "production_notes": self.production_notes, "quality_notes": self.quality_notes, "delay_reason": self.delay_reason, "cancellation_reason": self.cancellation_reason, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, "completed_at": self.completed_at.isoformat() if self.completed_at else None, } class ProductionSchedule(Base): """Production schedule model for planning and tracking daily production""" __tablename__ = "production_schedules" # 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) # Schedule information schedule_date = Column(DateTime(timezone=True), nullable=False, index=True) shift_start = Column(DateTime(timezone=True), nullable=False) shift_end = Column(DateTime(timezone=True), nullable=False) # Capacity planning total_capacity_hours = Column(Float, nullable=False) planned_capacity_hours = Column(Float, nullable=False) actual_capacity_hours = Column(Float, nullable=True) overtime_hours = Column(Float, nullable=True, default=0.0) # Staff and equipment staff_count = Column(Integer, nullable=False) equipment_capacity = Column(JSON, nullable=True) # Equipment availability station_assignments = Column(JSON, nullable=True) # Station schedules # Production metrics total_batches_planned = Column(Integer, nullable=False, default=0) total_batches_completed = Column(Integer, nullable=True, default=0) total_quantity_planned = Column(Float, nullable=False, default=0.0) total_quantity_produced = Column(Float, nullable=True, default=0.0) # Status tracking is_finalized = Column(Boolean, default=False) is_active = Column(Boolean, default=True) # Performance metrics efficiency_percentage = Column(Float, nullable=True) utilization_percentage = Column(Float, nullable=True) on_time_completion_rate = Column(Float, nullable=True) # Notes and adjustments schedule_notes = Column(Text, nullable=True) schedule_adjustments = Column(JSON, nullable=True) # Track changes made # Timestamps created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) finalized_at = Column(DateTime(timezone=True), nullable=True) def to_dict(self) -> Dict[str, Any]: """Convert to dictionary following shared pattern""" return { "id": str(self.id), "tenant_id": str(self.tenant_id), "schedule_date": self.schedule_date.isoformat() if self.schedule_date else None, "shift_start": self.shift_start.isoformat() if self.shift_start else None, "shift_end": self.shift_end.isoformat() if self.shift_end else None, "total_capacity_hours": self.total_capacity_hours, "planned_capacity_hours": self.planned_capacity_hours, "actual_capacity_hours": self.actual_capacity_hours, "overtime_hours": self.overtime_hours, "staff_count": self.staff_count, "equipment_capacity": self.equipment_capacity, "station_assignments": self.station_assignments, "total_batches_planned": self.total_batches_planned, "total_batches_completed": self.total_batches_completed, "total_quantity_planned": self.total_quantity_planned, "total_quantity_produced": self.total_quantity_produced, "is_finalized": self.is_finalized, "is_active": self.is_active, "efficiency_percentage": self.efficiency_percentage, "utilization_percentage": self.utilization_percentage, "on_time_completion_rate": self.on_time_completion_rate, "schedule_notes": self.schedule_notes, "schedule_adjustments": self.schedule_adjustments, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, "finalized_at": self.finalized_at.isoformat() if self.finalized_at else None, } class ProductionCapacity(Base): """Production capacity model for tracking equipment and resource availability""" __tablename__ = "production_capacity" # 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) # Capacity definition resource_type = Column(String(50), nullable=False) # equipment, staff, station resource_id = Column(String(100), nullable=False) resource_name = Column(String(255), nullable=False) # Time period date = Column(DateTime(timezone=True), nullable=False, index=True) start_time = Column(DateTime(timezone=True), nullable=False) end_time = Column(DateTime(timezone=True), nullable=False) # Capacity metrics total_capacity_units = Column(Float, nullable=False) # Total available capacity allocated_capacity_units = Column(Float, nullable=False, default=0.0) remaining_capacity_units = Column(Float, nullable=False) # Status is_available = Column(Boolean, default=True) is_maintenance = Column(Boolean, default=False) is_reserved = Column(Boolean, default=False) # Equipment specific equipment_type = Column(String(100), nullable=True) max_batch_size = Column(Float, nullable=True) min_batch_size = Column(Float, nullable=True) setup_time_minutes = Column(Integer, nullable=True) cleanup_time_minutes = Column(Integer, nullable=True) # Performance tracking efficiency_rating = Column(Float, nullable=True) maintenance_status = Column(String(50), nullable=True) last_maintenance_date = Column(DateTime(timezone=True), nullable=True) # Notes notes = Column(Text, nullable=True) restrictions = Column(JSON, nullable=True) # Product type restrictions, etc. # Timestamps created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) def to_dict(self) -> Dict[str, Any]: """Convert to dictionary following shared pattern""" return { "id": str(self.id), "tenant_id": str(self.tenant_id), "resource_type": self.resource_type, "resource_id": self.resource_id, "resource_name": self.resource_name, "date": self.date.isoformat() if self.date else None, "start_time": self.start_time.isoformat() if self.start_time else None, "end_time": self.end_time.isoformat() if self.end_time else None, "total_capacity_units": self.total_capacity_units, "allocated_capacity_units": self.allocated_capacity_units, "remaining_capacity_units": self.remaining_capacity_units, "is_available": self.is_available, "is_maintenance": self.is_maintenance, "is_reserved": self.is_reserved, "equipment_type": self.equipment_type, "max_batch_size": self.max_batch_size, "min_batch_size": self.min_batch_size, "setup_time_minutes": self.setup_time_minutes, "cleanup_time_minutes": self.cleanup_time_minutes, "efficiency_rating": self.efficiency_rating, "maintenance_status": self.maintenance_status, "last_maintenance_date": self.last_maintenance_date.isoformat() if self.last_maintenance_date else None, "notes": self.notes, "restrictions": self.restrictions, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, } class QualityCheck(Base): """Quality check model for tracking production quality metrics""" __tablename__ = "quality_checks" # 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) batch_id = Column(UUID(as_uuid=True), nullable=False, index=True) # FK to ProductionBatch # Check information check_type = Column(String(50), nullable=False) # visual, weight, temperature, etc. check_time = Column(DateTime(timezone=True), nullable=False) checker_id = Column(String(100), nullable=True) # Staff member who performed check # Quality metrics quality_score = Column(Float, nullable=False) # 1-10 scale pass_fail = Column(Boolean, nullable=False) defect_count = Column(Integer, nullable=False, default=0) defect_types = Column(JSON, nullable=True) # List of defect categories # Measurements measured_weight = Column(Float, nullable=True) measured_temperature = Column(Float, nullable=True) measured_moisture = Column(Float, nullable=True) measured_dimensions = Column(JSON, nullable=True) # Standards comparison target_weight = Column(Float, nullable=True) target_temperature = Column(Float, nullable=True) target_moisture = Column(Float, nullable=True) tolerance_percentage = Column(Float, nullable=True) # Results within_tolerance = Column(Boolean, nullable=True) corrective_action_needed = Column(Boolean, default=False) corrective_actions = Column(JSON, nullable=True) # Notes and documentation check_notes = Column(Text, nullable=True) photos_urls = Column(JSON, nullable=True) # URLs to quality check photos certificate_url = Column(String(500), nullable=True) # Timestamps created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) def to_dict(self) -> Dict[str, Any]: """Convert to dictionary following shared pattern""" return { "id": str(self.id), "tenant_id": str(self.tenant_id), "batch_id": str(self.batch_id), "check_type": self.check_type, "check_time": self.check_time.isoformat() if self.check_time else None, "checker_id": self.checker_id, "quality_score": self.quality_score, "pass_fail": self.pass_fail, "defect_count": self.defect_count, "defect_types": self.defect_types, "measured_weight": self.measured_weight, "measured_temperature": self.measured_temperature, "measured_moisture": self.measured_moisture, "measured_dimensions": self.measured_dimensions, "target_weight": self.target_weight, "target_temperature": self.target_temperature, "target_moisture": self.target_moisture, "tolerance_percentage": self.tolerance_percentage, "within_tolerance": self.within_tolerance, "corrective_action_needed": self.corrective_action_needed, "corrective_actions": self.corrective_actions, "check_notes": self.check_notes, "photos_urls": self.photos_urls, "certificate_url": self.certificate_url, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, }