# ================================================================ # 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 EquipmentStatus(str, enum.Enum): """Equipment status enumeration""" OPERATIONAL = "operational" MAINTENANCE = "maintenance" DOWN = "down" WARNING = "warning" class ProcessStage(str, enum.Enum): """Production process stages where quality checks can occur""" MIXING = "mixing" PROOFING = "proofing" SHAPING = "shaping" BAKING = "baking" COOLING = "cooling" PACKAGING = "packaging" FINISHING = "finishing" class EquipmentType(str, enum.Enum): """Equipment type enumeration""" OVEN = "oven" MIXER = "mixer" PROOFER = "proofer" FREEZER = "freezer" PACKAGING = "packaging" OTHER = "other" 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) # Process stage tracking current_process_stage = Column(SQLEnum(ProcessStage), nullable=True, index=True) process_stage_history = Column(JSON, nullable=True) # Track stage transitions with timestamps pending_quality_checks = Column(JSON, nullable=True) # Required quality checks for current stage completed_quality_checks = Column(JSON, nullable=True) # Completed quality checks by stage # 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 QualityCheckTemplate(Base): """Quality check templates for tenant-specific quality standards""" __tablename__ = "quality_check_templates" # 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) # Template identification name = Column(String(255), nullable=False) template_code = Column(String(100), nullable=True, index=True) check_type = Column(String(50), nullable=False) # visual, measurement, temperature, weight, boolean category = Column(String(100), nullable=True) # appearance, structure, texture, etc. # Template configuration description = Column(Text, nullable=True) instructions = Column(Text, nullable=True) parameters = Column(JSON, nullable=True) # Dynamic check parameters thresholds = Column(JSON, nullable=True) # Pass/fail criteria scoring_criteria = Column(JSON, nullable=True) # Scoring methodology # Configurability settings is_active = Column(Boolean, default=True) is_required = Column(Boolean, default=False) is_critical = Column(Boolean, default=False) # Critical failures block production weight = Column(Float, default=1.0) # Weight in overall quality score # Measurement specifications min_value = Column(Float, nullable=True) max_value = Column(Float, nullable=True) target_value = Column(Float, nullable=True) unit = Column(String(20), nullable=True) tolerance_percentage = Column(Float, nullable=True) # Process stage applicability applicable_stages = Column(JSON, nullable=True) # List of ProcessStage values # Metadata created_by = Column(UUID(as_uuid=True), nullable=False) 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), "name": self.name, "template_code": self.template_code, "check_type": self.check_type, "category": self.category, "description": self.description, "instructions": self.instructions, "parameters": self.parameters, "thresholds": self.thresholds, "scoring_criteria": self.scoring_criteria, "is_active": self.is_active, "is_required": self.is_required, "is_critical": self.is_critical, "weight": self.weight, "min_value": self.min_value, "max_value": self.max_value, "target_value": self.target_value, "unit": self.unit, "tolerance_percentage": self.tolerance_percentage, "applicable_stages": self.applicable_stages, "created_by": str(self.created_by), "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 with stage support""" __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 template_id = Column(UUID(as_uuid=True), nullable=True, index=True) # FK to QualityCheckTemplate # Check information check_type = Column(String(50), nullable=False) # visual, weight, temperature, etc. process_stage = Column(SQLEnum(ProcessStage), nullable=True, index=True) # Stage when check was performed 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) stage_specific_data = Column(JSON, nullable=True) # Stage-specific measurements # 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) # Template-based results template_results = Column(JSON, nullable=True) # Results from template-based checks criteria_scores = Column(JSON, nullable=True) # Individual criteria scores # 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, } class Equipment(Base): """Equipment model for tracking production equipment""" __tablename__ = "equipment" # 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) # Equipment identification name = Column(String(255), nullable=False) type = Column(SQLEnum(EquipmentType), nullable=False) model = Column(String(100), nullable=True) serial_number = Column(String(100), nullable=True) location = Column(String(255), nullable=True) # Status tracking status = Column(SQLEnum(EquipmentStatus), nullable=False, default=EquipmentStatus.OPERATIONAL) # Dates install_date = Column(DateTime(timezone=True), nullable=True) last_maintenance_date = Column(DateTime(timezone=True), nullable=True) next_maintenance_date = Column(DateTime(timezone=True), nullable=True) maintenance_interval_days = Column(Integer, nullable=True) # Maintenance interval in days # Performance metrics efficiency_percentage = Column(Float, nullable=True) # Current efficiency uptime_percentage = Column(Float, nullable=True) # Overall equipment effectiveness energy_usage_kwh = Column(Float, nullable=True) # Current energy usage # Specifications power_kw = Column(Float, nullable=True) # Power in kilowatts capacity = Column(Float, nullable=True) # Capacity (units depend on equipment type) weight_kg = Column(Float, nullable=True) # Weight in kilograms # Temperature monitoring current_temperature = Column(Float, nullable=True) # Current temperature reading target_temperature = Column(Float, nullable=True) # Target temperature # Status is_active = Column(Boolean, default=True) # Notes notes = Column(Text, 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), "name": self.name, "type": self.type.value if self.type else None, "model": self.model, "serial_number": self.serial_number, "location": self.location, "status": self.status.value if self.status else None, "install_date": self.install_date.isoformat() if self.install_date else None, "last_maintenance_date": self.last_maintenance_date.isoformat() if self.last_maintenance_date else None, "next_maintenance_date": self.next_maintenance_date.isoformat() if self.next_maintenance_date else None, "maintenance_interval_days": self.maintenance_interval_days, "efficiency_percentage": self.efficiency_percentage, "uptime_percentage": self.uptime_percentage, "energy_usage_kwh": self.energy_usage_kwh, "power_kw": self.power_kw, "capacity": self.capacity, "weight_kg": self.weight_kg, "current_temperature": self.current_temperature, "target_temperature": self.target_temperature, "is_active": self.is_active, "notes": self.notes, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, }