Add improved production UI 3
This commit is contained in:
@@ -22,6 +22,7 @@ from app.schemas.production import (
|
||||
ProductionStatusEnum
|
||||
)
|
||||
from app.core.config import settings
|
||||
from .quality_templates import router as quality_templates_router
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
174
services/production/app/api/quality_templates.py
Normal file
174
services/production/app/api/quality_templates.py
Normal file
@@ -0,0 +1,174 @@
|
||||
# services/production/app/api/quality_templates.py
|
||||
"""
|
||||
Quality Check Template API endpoints for production service
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from ..core.database import get_db
|
||||
from ..models.production import QualityCheckTemplate, ProcessStage
|
||||
from ..services.quality_template_service import QualityTemplateService
|
||||
from ..schemas.quality_templates import (
|
||||
QualityCheckTemplateCreate,
|
||||
QualityCheckTemplateUpdate,
|
||||
QualityCheckTemplateResponse,
|
||||
QualityCheckTemplateList
|
||||
)
|
||||
from shared.auth.tenant_access import get_current_tenant_id
|
||||
|
||||
router = APIRouter(prefix="/quality-templates", tags=["quality-templates"])
|
||||
|
||||
|
||||
@router.post("", response_model=QualityCheckTemplateResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_quality_template(
|
||||
template_data: QualityCheckTemplateCreate,
|
||||
tenant_id: str = Depends(get_current_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new quality check template"""
|
||||
try:
|
||||
service = QualityTemplateService(db)
|
||||
template = await service.create_template(tenant_id, template_data)
|
||||
return QualityCheckTemplateResponse.from_orm(template)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
|
||||
@router.get("", response_model=QualityCheckTemplateList)
|
||||
async def get_quality_templates(
|
||||
tenant_id: str = Depends(get_current_tenant_id),
|
||||
stage: Optional[ProcessStage] = Query(None, description="Filter by process stage"),
|
||||
check_type: Optional[str] = Query(None, description="Filter by check type"),
|
||||
is_active: Optional[bool] = Query(True, description="Filter by active status"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get quality check templates for tenant"""
|
||||
try:
|
||||
service = QualityTemplateService(db)
|
||||
templates, total = await service.get_templates(
|
||||
tenant_id=tenant_id,
|
||||
stage=stage,
|
||||
check_type=check_type,
|
||||
is_active=is_active,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
return QualityCheckTemplateList(
|
||||
templates=[QualityCheckTemplateResponse.from_orm(t) for t in templates],
|
||||
total=total,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{template_id}", response_model=QualityCheckTemplateResponse)
|
||||
async def get_quality_template(
|
||||
template_id: UUID,
|
||||
tenant_id: str = Depends(get_current_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get a specific quality check template"""
|
||||
try:
|
||||
service = QualityTemplateService(db)
|
||||
template = await service.get_template(tenant_id, template_id)
|
||||
if not template:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
|
||||
|
||||
return QualityCheckTemplateResponse.from_orm(template)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{template_id}", response_model=QualityCheckTemplateResponse)
|
||||
async def update_quality_template(
|
||||
template_id: UUID,
|
||||
template_data: QualityCheckTemplateUpdate,
|
||||
tenant_id: str = Depends(get_current_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update a quality check template"""
|
||||
try:
|
||||
service = QualityTemplateService(db)
|
||||
template = await service.update_template(tenant_id, template_id, template_data)
|
||||
if not template:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
|
||||
|
||||
return QualityCheckTemplateResponse.from_orm(template)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_quality_template(
|
||||
template_id: UUID,
|
||||
tenant_id: str = Depends(get_current_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete a quality check template"""
|
||||
try:
|
||||
service = QualityTemplateService(db)
|
||||
success = await service.delete_template(tenant_id, template_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/stages/{stage}", response_model=QualityCheckTemplateList)
|
||||
async def get_templates_for_stage(
|
||||
stage: ProcessStage,
|
||||
tenant_id: str = Depends(get_current_tenant_id),
|
||||
is_active: Optional[bool] = Query(True, description="Filter by active status"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get quality check templates applicable to a specific process stage"""
|
||||
try:
|
||||
service = QualityTemplateService(db)
|
||||
templates = await service.get_templates_for_stage(tenant_id, stage, is_active)
|
||||
|
||||
return QualityCheckTemplateList(
|
||||
templates=[QualityCheckTemplateResponse.from_orm(t) for t in templates],
|
||||
total=len(templates),
|
||||
skip=0,
|
||||
limit=len(templates)
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{template_id}/duplicate", response_model=QualityCheckTemplateResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def duplicate_quality_template(
|
||||
template_id: UUID,
|
||||
tenant_id: str = Depends(get_current_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Duplicate an existing quality check template"""
|
||||
try:
|
||||
service = QualityTemplateService(db)
|
||||
template = await service.duplicate_template(tenant_id, template_id)
|
||||
if not template:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
|
||||
|
||||
return QualityCheckTemplateResponse.from_orm(template)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
@@ -14,6 +14,7 @@ import structlog
|
||||
from app.core.config import settings
|
||||
from app.core.database import init_database, get_db_health
|
||||
from app.api.production import router as production_router
|
||||
from app.api.quality_templates import router as quality_templates_router
|
||||
from app.services.production_alert_service import ProductionAlertService
|
||||
|
||||
# Configure logging
|
||||
@@ -73,6 +74,7 @@ app.add_middleware(
|
||||
|
||||
# Include routers
|
||||
app.include_router(production_router, prefix="/api/v1")
|
||||
app.include_router(quality_templates_router, prefix="/api/v1")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
|
||||
@@ -35,6 +35,22 @@ class ProductionPriority(str, enum.Enum):
|
||||
URGENT = "URGENT"
|
||||
|
||||
|
||||
class EquipmentStatus(str, enum.Enum):
|
||||
"""Equipment status enumeration"""
|
||||
OPERATIONAL = "operational"
|
||||
MAINTENANCE = "maintenance"
|
||||
DOWN = "down"
|
||||
WARNING = "warning"
|
||||
|
||||
|
||||
class EquipmentType(str, enum.Enum):
|
||||
"""Equipment type enumeration"""
|
||||
OVEN = "oven"
|
||||
MIXER = "mixer"
|
||||
PROOFER = "proofer"
|
||||
FREEZER = "freezer"
|
||||
PACKAGING = "packaging"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class ProductionBatch(Base):
|
||||
@@ -56,16 +72,22 @@ class ProductionBatch(Base):
|
||||
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)
|
||||
@@ -307,48 +329,138 @@ class ProductionCapacity(Base):
|
||||
}
|
||||
|
||||
|
||||
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 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"""
|
||||
"""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())
|
||||
@@ -385,3 +497,81 @@ class QualityCheck(Base):
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
179
services/production/app/schemas/quality_templates.py
Normal file
179
services/production/app/schemas/quality_templates.py
Normal file
@@ -0,0 +1,179 @@
|
||||
# services/production/app/schemas/quality_templates.py
|
||||
"""
|
||||
Quality Check Template Pydantic schemas for validation and serialization
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import Optional, List, Dict, Any, Union
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from ..models.production import ProcessStage
|
||||
|
||||
|
||||
class QualityCheckType(str, Enum):
|
||||
"""Quality check types"""
|
||||
VISUAL = "visual"
|
||||
MEASUREMENT = "measurement"
|
||||
TEMPERATURE = "temperature"
|
||||
WEIGHT = "weight"
|
||||
BOOLEAN = "boolean"
|
||||
TIMING = "timing"
|
||||
|
||||
|
||||
class QualityCheckTemplateBase(BaseModel):
|
||||
"""Base schema for quality check templates"""
|
||||
name: str = Field(..., min_length=1, max_length=255, description="Template name")
|
||||
template_code: Optional[str] = Field(None, max_length=100, description="Template code for reference")
|
||||
check_type: QualityCheckType = Field(..., description="Type of quality check")
|
||||
category: Optional[str] = Field(None, max_length=100, description="Check category (e.g., appearance, structure)")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
instructions: Optional[str] = Field(None, description="Check instructions for staff")
|
||||
|
||||
# Configuration
|
||||
parameters: Optional[Dict[str, Any]] = Field(None, description="Dynamic check parameters")
|
||||
thresholds: Optional[Dict[str, Any]] = Field(None, description="Pass/fail criteria")
|
||||
scoring_criteria: Optional[Dict[str, Any]] = Field(None, description="Scoring methodology")
|
||||
|
||||
# Settings
|
||||
is_active: bool = Field(True, description="Whether template is active")
|
||||
is_required: bool = Field(False, description="Whether check is required")
|
||||
is_critical: bool = Field(False, description="Whether failure blocks production")
|
||||
weight: float = Field(1.0, ge=0.0, le=10.0, description="Weight in overall quality score")
|
||||
|
||||
# Measurement specifications
|
||||
min_value: Optional[float] = Field(None, description="Minimum acceptable value")
|
||||
max_value: Optional[float] = Field(None, description="Maximum acceptable value")
|
||||
target_value: Optional[float] = Field(None, description="Target value")
|
||||
unit: Optional[str] = Field(None, max_length=20, description="Unit of measurement")
|
||||
tolerance_percentage: Optional[float] = Field(None, ge=0.0, le=100.0, description="Tolerance percentage")
|
||||
|
||||
# Process stage applicability
|
||||
applicable_stages: Optional[List[ProcessStage]] = Field(None, description="Applicable process stages")
|
||||
|
||||
@validator('applicable_stages')
|
||||
def validate_stages(cls, v):
|
||||
if v is not None:
|
||||
# Ensure all values are valid ProcessStage enums
|
||||
for stage in v:
|
||||
if stage not in ProcessStage:
|
||||
raise ValueError(f"Invalid process stage: {stage}")
|
||||
return v
|
||||
|
||||
@validator('min_value', 'max_value', 'target_value')
|
||||
def validate_measurement_values(cls, v, values):
|
||||
if v is not None and values.get('check_type') not in [QualityCheckType.MEASUREMENT, QualityCheckType.TEMPERATURE, QualityCheckType.WEIGHT]:
|
||||
return None # Clear values for non-measurement types
|
||||
return v
|
||||
|
||||
|
||||
class QualityCheckTemplateCreate(QualityCheckTemplateBase):
|
||||
"""Schema for creating quality check templates"""
|
||||
created_by: UUID = Field(..., description="User ID who created the template")
|
||||
|
||||
|
||||
class QualityCheckTemplateUpdate(BaseModel):
|
||||
"""Schema for updating quality check templates"""
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
template_code: Optional[str] = Field(None, max_length=100)
|
||||
check_type: Optional[QualityCheckType] = None
|
||||
category: Optional[str] = Field(None, max_length=100)
|
||||
description: Optional[str] = None
|
||||
instructions: Optional[str] = None
|
||||
parameters: Optional[Dict[str, Any]] = None
|
||||
thresholds: Optional[Dict[str, Any]] = None
|
||||
scoring_criteria: Optional[Dict[str, Any]] = None
|
||||
is_active: Optional[bool] = None
|
||||
is_required: Optional[bool] = None
|
||||
is_critical: Optional[bool] = None
|
||||
weight: Optional[float] = Field(None, ge=0.0, le=10.0)
|
||||
min_value: Optional[float] = None
|
||||
max_value: Optional[float] = None
|
||||
target_value: Optional[float] = None
|
||||
unit: Optional[str] = Field(None, max_length=20)
|
||||
tolerance_percentage: Optional[float] = Field(None, ge=0.0, le=100.0)
|
||||
applicable_stages: Optional[List[ProcessStage]] = None
|
||||
|
||||
|
||||
class QualityCheckTemplateResponse(QualityCheckTemplateBase):
|
||||
"""Schema for quality check template responses"""
|
||||
id: UUID
|
||||
tenant_id: UUID
|
||||
created_by: UUID
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class QualityCheckTemplateList(BaseModel):
|
||||
"""Schema for paginated quality check template lists"""
|
||||
templates: List[QualityCheckTemplateResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
class QualityCheckCriterion(BaseModel):
|
||||
"""Individual quality check criterion within a template"""
|
||||
id: str = Field(..., description="Unique criterion identifier")
|
||||
name: str = Field(..., description="Criterion name")
|
||||
description: str = Field(..., description="Criterion description")
|
||||
check_type: QualityCheckType = Field(..., description="Type of check")
|
||||
required: bool = Field(True, description="Whether criterion is required")
|
||||
weight: float = Field(1.0, ge=0.0, le=10.0, description="Weight in template score")
|
||||
acceptable_criteria: str = Field(..., description="Description of acceptable criteria")
|
||||
min_value: Optional[float] = None
|
||||
max_value: Optional[float] = None
|
||||
unit: Optional[str] = None
|
||||
is_critical: bool = Field(False, description="Whether failure is critical")
|
||||
|
||||
|
||||
class QualityCheckResult(BaseModel):
|
||||
"""Result of a quality check criterion"""
|
||||
criterion_id: str = Field(..., description="Criterion identifier")
|
||||
value: Union[float, str, bool] = Field(..., description="Check result value")
|
||||
score: float = Field(..., ge=0.0, le=10.0, description="Score for this criterion")
|
||||
notes: Optional[str] = Field(None, description="Additional notes")
|
||||
photos: Optional[List[str]] = Field(None, description="Photo URLs")
|
||||
pass_check: bool = Field(..., description="Whether criterion passed")
|
||||
timestamp: datetime = Field(..., description="When check was performed")
|
||||
|
||||
|
||||
class QualityCheckExecutionRequest(BaseModel):
|
||||
"""Schema for executing a quality check using a template"""
|
||||
template_id: UUID = Field(..., description="Quality check template ID")
|
||||
batch_id: UUID = Field(..., description="Production batch ID")
|
||||
process_stage: ProcessStage = Field(..., description="Current process stage")
|
||||
checker_id: Optional[str] = Field(None, description="Staff member performing check")
|
||||
results: List[QualityCheckResult] = Field(..., description="Check results")
|
||||
final_notes: Optional[str] = Field(None, description="Final notes")
|
||||
photos: Optional[List[str]] = Field(None, description="Additional photo URLs")
|
||||
|
||||
|
||||
class QualityCheckExecutionResponse(BaseModel):
|
||||
"""Schema for quality check execution results"""
|
||||
check_id: UUID = Field(..., description="Created quality check ID")
|
||||
overall_score: float = Field(..., ge=0.0, le=10.0, description="Overall quality score")
|
||||
overall_pass: bool = Field(..., description="Whether check passed overall")
|
||||
critical_failures: List[str] = Field(..., description="List of critical failures")
|
||||
corrective_actions: List[str] = Field(..., description="Recommended corrective actions")
|
||||
timestamp: datetime = Field(..., description="When check was completed")
|
||||
|
||||
|
||||
class ProcessStageQualityConfig(BaseModel):
|
||||
"""Configuration for quality checks at a specific process stage"""
|
||||
stage: ProcessStage = Field(..., description="Process stage")
|
||||
template_ids: List[UUID] = Field(..., description="Required template IDs")
|
||||
custom_parameters: Optional[Dict[str, Any]] = Field(None, description="Stage-specific parameters")
|
||||
is_required: bool = Field(True, description="Whether stage requires quality checks")
|
||||
blocking: bool = Field(True, description="Whether stage blocks on failed checks")
|
||||
|
||||
|
||||
class RecipeQualityConfiguration(BaseModel):
|
||||
"""Quality check configuration for a recipe"""
|
||||
stages: Dict[str, ProcessStageQualityConfig] = Field(..., description="Stage configurations")
|
||||
global_parameters: Optional[Dict[str, Any]] = Field(None, description="Global quality parameters")
|
||||
default_templates: Optional[List[UUID]] = Field(None, description="Default template IDs")
|
||||
@@ -49,14 +49,14 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
|
||||
max_instances=1
|
||||
)
|
||||
|
||||
# Equipment monitoring - disabled (equipment tables not available in production database)
|
||||
# self.scheduler.add_job(
|
||||
# self.check_equipment_status,
|
||||
# CronTrigger(minute='*/3'),
|
||||
# id='equipment_check',
|
||||
# misfire_grace_time=30,
|
||||
# max_instances=1
|
||||
# )
|
||||
# Equipment monitoring - check equipment status for maintenance alerts
|
||||
self.scheduler.add_job(
|
||||
self.check_equipment_status,
|
||||
CronTrigger(minute='*/30'), # Check every 30 minutes
|
||||
id='equipment_check',
|
||||
misfire_grace_time=30,
|
||||
max_instances=1
|
||||
)
|
||||
|
||||
# Efficiency recommendations - every 30 minutes (recommendations)
|
||||
self.scheduler.add_job(
|
||||
@@ -394,19 +394,61 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
|
||||
error=str(e))
|
||||
|
||||
async def check_equipment_status(self):
|
||||
"""Check equipment status and failures (alerts)"""
|
||||
# Equipment tables don't exist in production database - skip this check
|
||||
logger.debug("Equipment check skipped - equipment tables not available in production database")
|
||||
return
|
||||
"""Check equipment status and maintenance requirements (alerts)"""
|
||||
try:
|
||||
self._checks_performed += 1
|
||||
|
||||
# Query equipment that needs attention
|
||||
query = """
|
||||
SELECT
|
||||
e.id, e.tenant_id, e.name, e.type, e.status,
|
||||
e.efficiency_percentage, e.uptime_percentage,
|
||||
e.last_maintenance_date, e.next_maintenance_date,
|
||||
e.maintenance_interval_days,
|
||||
EXTRACT(DAYS FROM (e.next_maintenance_date - NOW())) as days_to_maintenance,
|
||||
COUNT(ea.id) as active_alerts
|
||||
FROM equipment e
|
||||
LEFT JOIN alerts ea ON ea.equipment_id = e.id
|
||||
AND ea.is_active = true
|
||||
AND ea.is_resolved = false
|
||||
WHERE e.is_active = true
|
||||
AND e.tenant_id = $1
|
||||
GROUP BY e.id, e.tenant_id, e.name, e.type, e.status,
|
||||
e.efficiency_percentage, e.uptime_percentage,
|
||||
e.last_maintenance_date, e.next_maintenance_date,
|
||||
e.maintenance_interval_days
|
||||
ORDER BY e.next_maintenance_date ASC
|
||||
"""
|
||||
|
||||
tenants = await self.get_active_tenants()
|
||||
|
||||
for tenant_id in tenants:
|
||||
try:
|
||||
from sqlalchemy import text
|
||||
async with self.db_manager.get_session() as session:
|
||||
result = await session.execute(text(query), {"tenant_id": tenant_id})
|
||||
equipment_list = result.fetchall()
|
||||
|
||||
for equipment in equipment_list:
|
||||
await self._process_equipment_issue(equipment)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error checking equipment status",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Equipment status check failed", error=str(e))
|
||||
self._errors_count += 1
|
||||
|
||||
async def _process_equipment_issue(self, equipment: Dict[str, Any]):
|
||||
"""Process equipment issue"""
|
||||
try:
|
||||
status = equipment['status']
|
||||
efficiency = equipment.get('efficiency_percent', 100)
|
||||
efficiency = equipment.get('efficiency_percentage', 100)
|
||||
days_to_maintenance = equipment.get('days_to_maintenance', 30)
|
||||
|
||||
if status == 'error':
|
||||
if status == 'down':
|
||||
template_data = self.format_spanish_message(
|
||||
'equipment_failure',
|
||||
equipment_name=equipment['name']
|
||||
@@ -422,41 +464,52 @@ class ProductionAlertService(BaseAlertService, AlertServiceMixin):
|
||||
'equipment_id': str(equipment['id']),
|
||||
'equipment_name': equipment['name'],
|
||||
'equipment_type': equipment['type'],
|
||||
'error_count': equipment.get('error_count', 0),
|
||||
'last_reading': equipment.get('last_reading').isoformat() if equipment.get('last_reading') else None
|
||||
'efficiency': efficiency
|
||||
}
|
||||
}, item_type='alert')
|
||||
|
||||
elif status == 'maintenance_required' or days_to_maintenance <= 1:
|
||||
severity = 'high' if days_to_maintenance <= 1 else 'medium'
|
||||
elif status == 'maintenance' or (days_to_maintenance is not None and days_to_maintenance <= 3):
|
||||
severity = 'high' if (days_to_maintenance is not None and days_to_maintenance <= 1) else 'medium'
|
||||
|
||||
template_data = self.format_spanish_message(
|
||||
'maintenance_required',
|
||||
equipment_name=equipment['name'],
|
||||
days_until_maintenance=max(0, int(days_to_maintenance)) if days_to_maintenance is not None else 3
|
||||
)
|
||||
|
||||
await self.publish_item(equipment['tenant_id'], {
|
||||
'type': 'maintenance_required',
|
||||
'severity': severity,
|
||||
'title': f'🔧 Mantenimiento Requerido: {equipment["name"]}',
|
||||
'message': f'Equipo {equipment["name"]} requiere mantenimiento en {days_to_maintenance} días.',
|
||||
'actions': ['Programar mantenimiento', 'Revisar historial', 'Preparar repuestos', 'Planificar parada'],
|
||||
'title': template_data['title'],
|
||||
'message': template_data['message'],
|
||||
'actions': template_data['actions'],
|
||||
'metadata': {
|
||||
'equipment_id': str(equipment['id']),
|
||||
'equipment_name': equipment['name'],
|
||||
'days_to_maintenance': days_to_maintenance,
|
||||
'last_maintenance': equipment.get('last_maintenance').isoformat() if equipment.get('last_maintenance') else None
|
||||
'last_maintenance': equipment.get('last_maintenance_date')
|
||||
}
|
||||
}, item_type='alert')
|
||||
|
||||
elif efficiency < 80:
|
||||
elif efficiency is not None and efficiency < 80:
|
||||
severity = 'medium' if efficiency < 70 else 'low'
|
||||
|
||||
template_data = self.format_spanish_message(
|
||||
'low_equipment_efficiency',
|
||||
equipment_name=equipment['name'],
|
||||
efficiency_percent=round(efficiency, 1)
|
||||
)
|
||||
|
||||
await self.publish_item(equipment['tenant_id'], {
|
||||
'type': 'low_equipment_efficiency',
|
||||
'severity': severity,
|
||||
'title': f'📉 Baja Eficiencia: {equipment["name"]}',
|
||||
'message': f'Eficiencia del {equipment["name"]} bajó a {efficiency:.1f}%. Revisar funcionamiento.',
|
||||
'actions': ['Revisar configuración', 'Limpiar equipo', 'Calibrar sensores', 'Revisar mantenimiento'],
|
||||
'title': template_data['title'],
|
||||
'message': template_data['message'],
|
||||
'actions': template_data['actions'],
|
||||
'metadata': {
|
||||
'equipment_id': str(equipment['id']),
|
||||
'efficiency_percent': float(efficiency),
|
||||
'temperature': equipment.get('temperature'),
|
||||
'vibration_level': equipment.get('vibration_level')
|
||||
'equipment_name': equipment['name'],
|
||||
'efficiency_percent': float(efficiency)
|
||||
}
|
||||
}, item_type='alert')
|
||||
|
||||
|
||||
306
services/production/app/services/quality_template_service.py
Normal file
306
services/production/app/services/quality_template_service.py
Normal file
@@ -0,0 +1,306 @@
|
||||
# services/production/app/services/quality_template_service.py
|
||||
"""
|
||||
Quality Check Template Service for business logic and data operations
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_, func
|
||||
from typing import List, Optional, Tuple
|
||||
from uuid import UUID, uuid4
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from ..models.production import QualityCheckTemplate, ProcessStage
|
||||
from ..schemas.quality_templates import QualityCheckTemplateCreate, QualityCheckTemplateUpdate
|
||||
|
||||
|
||||
class QualityTemplateService:
|
||||
"""Service for managing quality check templates"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
async def create_template(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_data: QualityCheckTemplateCreate
|
||||
) -> QualityCheckTemplate:
|
||||
"""Create a new quality check template"""
|
||||
|
||||
# Validate template code uniqueness if provided
|
||||
if template_data.template_code:
|
||||
existing = self.db.query(QualityCheckTemplate).filter(
|
||||
and_(
|
||||
QualityCheckTemplate.tenant_id == tenant_id,
|
||||
QualityCheckTemplate.template_code == template_data.template_code
|
||||
)
|
||||
).first()
|
||||
if existing:
|
||||
raise ValueError(f"Template code '{template_data.template_code}' already exists")
|
||||
|
||||
# Create template
|
||||
template = QualityCheckTemplate(
|
||||
id=uuid4(),
|
||||
tenant_id=UUID(tenant_id),
|
||||
**template_data.dict()
|
||||
)
|
||||
|
||||
self.db.add(template)
|
||||
self.db.commit()
|
||||
self.db.refresh(template)
|
||||
|
||||
return template
|
||||
|
||||
async def get_templates(
|
||||
self,
|
||||
tenant_id: str,
|
||||
stage: Optional[ProcessStage] = None,
|
||||
check_type: Optional[str] = None,
|
||||
is_active: Optional[bool] = True,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> Tuple[List[QualityCheckTemplate], int]:
|
||||
"""Get quality check templates with filtering and pagination"""
|
||||
|
||||
query = self.db.query(QualityCheckTemplate).filter(
|
||||
QualityCheckTemplate.tenant_id == tenant_id
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if is_active is not None:
|
||||
query = query.filter(QualityCheckTemplate.is_active == is_active)
|
||||
|
||||
if check_type:
|
||||
query = query.filter(QualityCheckTemplate.check_type == check_type)
|
||||
|
||||
if stage:
|
||||
# Filter by applicable stages (JSON array contains stage)
|
||||
query = query.filter(
|
||||
func.json_contains(
|
||||
QualityCheckTemplate.applicable_stages,
|
||||
f'"{stage.value}"'
|
||||
)
|
||||
)
|
||||
|
||||
# Get total count
|
||||
total = query.count()
|
||||
|
||||
# Apply pagination and ordering
|
||||
templates = query.order_by(
|
||||
QualityCheckTemplate.is_critical.desc(),
|
||||
QualityCheckTemplate.is_required.desc(),
|
||||
QualityCheckTemplate.name
|
||||
).offset(skip).limit(limit).all()
|
||||
|
||||
return templates, total
|
||||
|
||||
async def get_template(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_id: UUID
|
||||
) -> Optional[QualityCheckTemplate]:
|
||||
"""Get a specific quality check template"""
|
||||
|
||||
return self.db.query(QualityCheckTemplate).filter(
|
||||
and_(
|
||||
QualityCheckTemplate.tenant_id == tenant_id,
|
||||
QualityCheckTemplate.id == template_id
|
||||
)
|
||||
).first()
|
||||
|
||||
async def update_template(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_id: UUID,
|
||||
template_data: QualityCheckTemplateUpdate
|
||||
) -> Optional[QualityCheckTemplate]:
|
||||
"""Update a quality check template"""
|
||||
|
||||
template = await self.get_template(tenant_id, template_id)
|
||||
if not template:
|
||||
return None
|
||||
|
||||
# Validate template code uniqueness if being updated
|
||||
if template_data.template_code and template_data.template_code != template.template_code:
|
||||
existing = self.db.query(QualityCheckTemplate).filter(
|
||||
and_(
|
||||
QualityCheckTemplate.tenant_id == tenant_id,
|
||||
QualityCheckTemplate.template_code == template_data.template_code,
|
||||
QualityCheckTemplate.id != template_id
|
||||
)
|
||||
).first()
|
||||
if existing:
|
||||
raise ValueError(f"Template code '{template_data.template_code}' already exists")
|
||||
|
||||
# Update fields
|
||||
update_data = template_data.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(template, field, value)
|
||||
|
||||
template.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(template)
|
||||
|
||||
return template
|
||||
|
||||
async def delete_template(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_id: UUID
|
||||
) -> bool:
|
||||
"""Delete a quality check template"""
|
||||
|
||||
template = await self.get_template(tenant_id, template_id)
|
||||
if not template:
|
||||
return False
|
||||
|
||||
# Check if template is in use (you might want to add this check)
|
||||
# For now, we'll allow deletion but in production you might want to:
|
||||
# 1. Soft delete by setting is_active = False
|
||||
# 2. Check for dependent quality checks
|
||||
# 3. Prevent deletion if in use
|
||||
|
||||
self.db.delete(template)
|
||||
self.db.commit()
|
||||
|
||||
return True
|
||||
|
||||
async def get_templates_for_stage(
|
||||
self,
|
||||
tenant_id: str,
|
||||
stage: ProcessStage,
|
||||
is_active: Optional[bool] = True
|
||||
) -> List[QualityCheckTemplate]:
|
||||
"""Get all quality check templates applicable to a specific process stage"""
|
||||
|
||||
query = self.db.query(QualityCheckTemplate).filter(
|
||||
and_(
|
||||
QualityCheckTemplate.tenant_id == tenant_id,
|
||||
or_(
|
||||
# Templates that specify applicable stages
|
||||
func.json_contains(
|
||||
QualityCheckTemplate.applicable_stages,
|
||||
f'"{stage.value}"'
|
||||
),
|
||||
# Templates that don't specify stages (applicable to all)
|
||||
QualityCheckTemplate.applicable_stages.is_(None)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if is_active is not None:
|
||||
query = query.filter(QualityCheckTemplate.is_active == is_active)
|
||||
|
||||
return query.order_by(
|
||||
QualityCheckTemplate.is_critical.desc(),
|
||||
QualityCheckTemplate.is_required.desc(),
|
||||
QualityCheckTemplate.weight.desc(),
|
||||
QualityCheckTemplate.name
|
||||
).all()
|
||||
|
||||
async def duplicate_template(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_id: UUID
|
||||
) -> Optional[QualityCheckTemplate]:
|
||||
"""Duplicate an existing quality check template"""
|
||||
|
||||
original = await self.get_template(tenant_id, template_id)
|
||||
if not original:
|
||||
return None
|
||||
|
||||
# Create duplicate with modified name and code
|
||||
duplicate_data = {
|
||||
'name': f"{original.name} (Copy)",
|
||||
'template_code': f"{original.template_code}_copy" if original.template_code else None,
|
||||
'check_type': original.check_type,
|
||||
'category': original.category,
|
||||
'description': original.description,
|
||||
'instructions': original.instructions,
|
||||
'parameters': original.parameters,
|
||||
'thresholds': original.thresholds,
|
||||
'scoring_criteria': original.scoring_criteria,
|
||||
'is_active': original.is_active,
|
||||
'is_required': original.is_required,
|
||||
'is_critical': original.is_critical,
|
||||
'weight': original.weight,
|
||||
'min_value': original.min_value,
|
||||
'max_value': original.max_value,
|
||||
'target_value': original.target_value,
|
||||
'unit': original.unit,
|
||||
'tolerance_percentage': original.tolerance_percentage,
|
||||
'applicable_stages': original.applicable_stages,
|
||||
'created_by': original.created_by
|
||||
}
|
||||
|
||||
create_data = QualityCheckTemplateCreate(**duplicate_data)
|
||||
return await self.create_template(tenant_id, create_data)
|
||||
|
||||
async def get_templates_by_recipe_config(
|
||||
self,
|
||||
tenant_id: str,
|
||||
stage: ProcessStage,
|
||||
recipe_quality_config: dict
|
||||
) -> List[QualityCheckTemplate]:
|
||||
"""Get quality check templates based on recipe configuration"""
|
||||
|
||||
# Extract template IDs from recipe configuration for the specific stage
|
||||
stage_config = recipe_quality_config.get('stages', {}).get(stage.value)
|
||||
if not stage_config:
|
||||
return []
|
||||
|
||||
template_ids = stage_config.get('template_ids', [])
|
||||
if not template_ids:
|
||||
return []
|
||||
|
||||
# Get templates by IDs
|
||||
templates = self.db.query(QualityCheckTemplate).filter(
|
||||
and_(
|
||||
QualityCheckTemplate.tenant_id == tenant_id,
|
||||
QualityCheckTemplate.id.in_([UUID(tid) for tid in template_ids]),
|
||||
QualityCheckTemplate.is_active == True
|
||||
)
|
||||
).order_by(
|
||||
QualityCheckTemplate.is_critical.desc(),
|
||||
QualityCheckTemplate.is_required.desc(),
|
||||
QualityCheckTemplate.weight.desc()
|
||||
).all()
|
||||
|
||||
return templates
|
||||
|
||||
async def validate_template_configuration(
|
||||
self,
|
||||
tenant_id: str,
|
||||
template_data: dict
|
||||
) -> Tuple[bool, List[str]]:
|
||||
"""Validate quality check template configuration"""
|
||||
|
||||
errors = []
|
||||
|
||||
# Validate check type specific requirements
|
||||
check_type = template_data.get('check_type')
|
||||
|
||||
if check_type in ['measurement', 'temperature', 'weight']:
|
||||
if not template_data.get('unit'):
|
||||
errors.append(f"Unit is required for {check_type} checks")
|
||||
|
||||
min_val = template_data.get('min_value')
|
||||
max_val = template_data.get('max_value')
|
||||
|
||||
if min_val is not None and max_val is not None and min_val >= max_val:
|
||||
errors.append("Minimum value must be less than maximum value")
|
||||
|
||||
# Validate scoring criteria
|
||||
scoring = template_data.get('scoring_criteria', {})
|
||||
if check_type == 'visual' and not scoring:
|
||||
errors.append("Visual checks require scoring criteria")
|
||||
|
||||
# Validate process stages
|
||||
stages = template_data.get('applicable_stages', [])
|
||||
if stages:
|
||||
valid_stages = [stage.value for stage in ProcessStage]
|
||||
invalid_stages = [s for s in stages if s not in valid_stages]
|
||||
if invalid_stages:
|
||||
errors.append(f"Invalid process stages: {invalid_stages}")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
Reference in New Issue
Block a user