Add improved production UI 3
This commit is contained in:
@@ -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