388 lines
18 KiB
Python
388 lines
18 KiB
Python
|
|
# ================================================================
|
||
|
|
# 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,
|
||
|
|
}
|
||
|
|
|
||
|
|
|