Improve demo seed
This commit is contained in:
@@ -9,7 +9,7 @@ from sqlalchemy import select
|
||||
import structlog
|
||||
import uuid
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional
|
||||
from typing import Optional, Dict, Any
|
||||
import os
|
||||
|
||||
from app.core.database import get_db
|
||||
@@ -19,6 +19,8 @@ from app.models.production import (
|
||||
ProductionStatus, ProductionPriority, ProcessStage,
|
||||
EquipmentStatus, EquipmentType
|
||||
)
|
||||
from shared.utils.demo_dates import adjust_date_for_demo, BASE_REFERENCE_DATE
|
||||
from shared.utils.alert_generator import generate_equipment_alerts
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter(prefix="/internal/demo", tags=["internal"])
|
||||
@@ -45,6 +47,7 @@ async def clone_demo_data(
|
||||
virtual_tenant_id: str,
|
||||
demo_account_type: str,
|
||||
session_id: Optional[str] = None,
|
||||
session_created_at: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: bool = Depends(verify_internal_api_key)
|
||||
):
|
||||
@@ -64,18 +67,29 @@ async def clone_demo_data(
|
||||
virtual_tenant_id: Target virtual tenant UUID
|
||||
demo_account_type: Type of demo account
|
||||
session_id: Originating session ID for tracing
|
||||
session_created_at: Session creation timestamp for date adjustment
|
||||
|
||||
Returns:
|
||||
Cloning status and record counts
|
||||
"""
|
||||
start_time = datetime.now(timezone.utc)
|
||||
|
||||
# Parse session creation time for date adjustment
|
||||
if session_created_at:
|
||||
try:
|
||||
session_time = datetime.fromisoformat(session_created_at.replace('Z', '+00:00'))
|
||||
except (ValueError, AttributeError):
|
||||
session_time = start_time
|
||||
else:
|
||||
session_time = start_time
|
||||
|
||||
logger.info(
|
||||
"Starting production data cloning",
|
||||
base_tenant_id=base_tenant_id,
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
demo_account_type=demo_account_type,
|
||||
session_id=session_id
|
||||
session_id=session_id,
|
||||
session_created_at=session_created_at
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -90,7 +104,8 @@ async def clone_demo_data(
|
||||
"production_capacity": 0,
|
||||
"quality_check_templates": 0,
|
||||
"quality_checks": 0,
|
||||
"equipment": 0
|
||||
"equipment": 0,
|
||||
"alerts_generated": 0
|
||||
}
|
||||
|
||||
# ID mappings
|
||||
@@ -114,6 +129,17 @@ async def clone_demo_data(
|
||||
new_equipment_id = uuid.uuid4()
|
||||
equipment_id_map[equipment.id] = new_equipment_id
|
||||
|
||||
# Adjust dates relative to session creation time
|
||||
adjusted_install_date = adjust_date_for_demo(
|
||||
equipment.install_date, session_time, BASE_REFERENCE_DATE
|
||||
)
|
||||
adjusted_last_maintenance = adjust_date_for_demo(
|
||||
equipment.last_maintenance_date, session_time, BASE_REFERENCE_DATE
|
||||
)
|
||||
adjusted_next_maintenance = adjust_date_for_demo(
|
||||
equipment.next_maintenance_date, session_time, BASE_REFERENCE_DATE
|
||||
)
|
||||
|
||||
new_equipment = Equipment(
|
||||
id=new_equipment_id,
|
||||
tenant_id=virtual_uuid,
|
||||
@@ -123,9 +149,9 @@ async def clone_demo_data(
|
||||
serial_number=equipment.serial_number,
|
||||
location=equipment.location,
|
||||
status=equipment.status,
|
||||
install_date=equipment.install_date,
|
||||
last_maintenance_date=equipment.last_maintenance_date,
|
||||
next_maintenance_date=equipment.next_maintenance_date,
|
||||
install_date=adjusted_install_date,
|
||||
last_maintenance_date=adjusted_last_maintenance,
|
||||
next_maintenance_date=adjusted_next_maintenance,
|
||||
maintenance_interval_days=equipment.maintenance_interval_days,
|
||||
efficiency_percentage=equipment.efficiency_percentage,
|
||||
uptime_percentage=equipment.uptime_percentage,
|
||||
@@ -137,8 +163,8 @@ async def clone_demo_data(
|
||||
target_temperature=equipment.target_temperature,
|
||||
is_active=equipment.is_active,
|
||||
notes=equipment.notes,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
created_at=session_time,
|
||||
updated_at=session_time
|
||||
)
|
||||
db.add(new_equipment)
|
||||
stats["equipment"] += 1
|
||||
@@ -185,8 +211,8 @@ async def clone_demo_data(
|
||||
tolerance_percentage=template.tolerance_percentage,
|
||||
applicable_stages=template.applicable_stages,
|
||||
created_by=template.created_by,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
created_at=session_time,
|
||||
updated_at=session_time
|
||||
)
|
||||
db.add(new_template)
|
||||
stats["quality_check_templates"] += 1
|
||||
@@ -403,9 +429,18 @@ async def clone_demo_data(
|
||||
db.add(new_capacity)
|
||||
stats["production_capacity"] += 1
|
||||
|
||||
# Commit all changes
|
||||
# Commit cloned data first
|
||||
await db.commit()
|
||||
|
||||
# Generate equipment maintenance and status alerts
|
||||
try:
|
||||
alerts_count = await generate_equipment_alerts(db, virtual_uuid, session_time)
|
||||
stats["alerts_generated"] += alerts_count
|
||||
await db.commit()
|
||||
logger.info(f"Generated {alerts_count} equipment alerts")
|
||||
except Exception as alert_error:
|
||||
logger.warning(f"Alert generation failed: {alert_error}", exc_info=True)
|
||||
|
||||
total_records = sum(stats.values())
|
||||
duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class QualityCheckType(str, Enum):
|
||||
WEIGHT = "weight"
|
||||
BOOLEAN = "boolean"
|
||||
TIMING = "timing"
|
||||
CHECKLIST = "checklist"
|
||||
|
||||
|
||||
class QualityCheckTemplateBase(BaseModel):
|
||||
|
||||
219
services/production/scripts/demo/equipos_es.json
Normal file
219
services/production/scripts/demo/equipos_es.json
Normal file
@@ -0,0 +1,219 @@
|
||||
{
|
||||
"equipos_individual_bakery": [
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000001",
|
||||
"name": "Horno Rotativo Principal",
|
||||
"type": "oven",
|
||||
"model": "Sveba Dahlen DC-16",
|
||||
"serial_number": "SD-2023-1547",
|
||||
"location": "Área de Producción - Zona A",
|
||||
"status": "operational",
|
||||
"power_kw": 45.0,
|
||||
"capacity": 16.0,
|
||||
"efficiency_percentage": 92.0,
|
||||
"current_temperature": 220.0,
|
||||
"target_temperature": 220.0,
|
||||
"maintenance_interval_days": 90,
|
||||
"last_maintenance_offset_days": -30,
|
||||
"install_date_offset_days": -730
|
||||
},
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000002",
|
||||
"name": "Amasadora Espiral Grande",
|
||||
"type": "mixer",
|
||||
"model": "Diosna SP 120",
|
||||
"serial_number": "DI-2022-0892",
|
||||
"location": "Área de Amasado",
|
||||
"status": "operational",
|
||||
"power_kw": 12.0,
|
||||
"capacity": 120.0,
|
||||
"efficiency_percentage": 95.0,
|
||||
"maintenance_interval_days": 60,
|
||||
"last_maintenance_offset_days": -15,
|
||||
"install_date_offset_days": -900
|
||||
},
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000003",
|
||||
"name": "Cámara de Fermentación 1",
|
||||
"type": "proofer",
|
||||
"model": "Mondial Forni PF-2000",
|
||||
"serial_number": "MF-2023-0445",
|
||||
"location": "Área de Fermentación",
|
||||
"status": "operational",
|
||||
"power_kw": 8.0,
|
||||
"capacity": 40.0,
|
||||
"efficiency_percentage": 88.0,
|
||||
"current_temperature": 28.0,
|
||||
"target_temperature": 28.0,
|
||||
"maintenance_interval_days": 90,
|
||||
"last_maintenance_offset_days": -45,
|
||||
"install_date_offset_days": -550
|
||||
},
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000004",
|
||||
"name": "Congelador Rápido",
|
||||
"type": "freezer",
|
||||
"model": "Irinox MF 70.2",
|
||||
"serial_number": "IR-2021-1234",
|
||||
"location": "Área de Conservación",
|
||||
"status": "operational",
|
||||
"power_kw": 15.0,
|
||||
"capacity": 70.0,
|
||||
"efficiency_percentage": 90.0,
|
||||
"current_temperature": -40.0,
|
||||
"target_temperature": -40.0,
|
||||
"maintenance_interval_days": 120,
|
||||
"last_maintenance_offset_days": -60,
|
||||
"install_date_offset_days": -1460
|
||||
},
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000005",
|
||||
"name": "Amasadora Pequeña",
|
||||
"type": "mixer",
|
||||
"model": "Diosna SP 60",
|
||||
"serial_number": "DI-2020-0334",
|
||||
"location": "Área de Amasado",
|
||||
"status": "warning",
|
||||
"power_kw": 6.0,
|
||||
"capacity": 60.0,
|
||||
"efficiency_percentage": 78.0,
|
||||
"maintenance_interval_days": 60,
|
||||
"last_maintenance_offset_days": -55,
|
||||
"install_date_offset_days": -1825,
|
||||
"notes": "Eficiencia reducida. Programar inspección preventiva."
|
||||
},
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000006",
|
||||
"name": "Horno de Convección Auxiliar",
|
||||
"type": "oven",
|
||||
"model": "Unox XBC 1065",
|
||||
"serial_number": "UN-2019-0667",
|
||||
"location": "Área de Producción - Zona B",
|
||||
"status": "operational",
|
||||
"power_kw": 28.0,
|
||||
"capacity": 10.0,
|
||||
"efficiency_percentage": 85.0,
|
||||
"current_temperature": 180.0,
|
||||
"target_temperature": 180.0,
|
||||
"maintenance_interval_days": 90,
|
||||
"last_maintenance_offset_days": -20,
|
||||
"install_date_offset_days": -2190
|
||||
}
|
||||
],
|
||||
"equipos_central_bakery": [
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000011",
|
||||
"name": "Línea de Producción Automática 1",
|
||||
"type": "other",
|
||||
"model": "Mecatherm TH 4500",
|
||||
"serial_number": "MT-2023-8890",
|
||||
"location": "Nave Principal - Línea 1",
|
||||
"status": "operational",
|
||||
"power_kw": 180.0,
|
||||
"capacity": 4500.0,
|
||||
"efficiency_percentage": 96.0,
|
||||
"maintenance_interval_days": 30,
|
||||
"last_maintenance_offset_days": -15,
|
||||
"install_date_offset_days": -400
|
||||
},
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000012",
|
||||
"name": "Horno Túnel Industrial",
|
||||
"type": "oven",
|
||||
"model": "Werner & Pfleiderer HS-3000",
|
||||
"serial_number": "WP-2022-1156",
|
||||
"location": "Nave Principal - Línea 1",
|
||||
"status": "operational",
|
||||
"power_kw": 250.0,
|
||||
"capacity": 3000.0,
|
||||
"efficiency_percentage": 94.0,
|
||||
"current_temperature": 230.0,
|
||||
"target_temperature": 230.0,
|
||||
"maintenance_interval_days": 45,
|
||||
"last_maintenance_offset_days": -20,
|
||||
"install_date_offset_days": -1095
|
||||
},
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000013",
|
||||
"name": "Amasadora Industrial Grande",
|
||||
"type": "mixer",
|
||||
"model": "Diosna SP 500",
|
||||
"serial_number": "DI-2023-1789",
|
||||
"location": "Zona de Amasado Industrial",
|
||||
"status": "operational",
|
||||
"power_kw": 75.0,
|
||||
"capacity": 500.0,
|
||||
"efficiency_percentage": 97.0,
|
||||
"maintenance_interval_days": 60,
|
||||
"last_maintenance_offset_days": -30,
|
||||
"install_date_offset_days": -365
|
||||
},
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000014",
|
||||
"name": "Cámara de Fermentación Industrial 1",
|
||||
"type": "proofer",
|
||||
"model": "Sveba Dahlen FC-800",
|
||||
"serial_number": "SD-2022-3344",
|
||||
"location": "Zona de Fermentación",
|
||||
"status": "operational",
|
||||
"power_kw": 45.0,
|
||||
"capacity": 800.0,
|
||||
"efficiency_percentage": 92.0,
|
||||
"current_temperature": 30.0,
|
||||
"target_temperature": 30.0,
|
||||
"maintenance_interval_days": 60,
|
||||
"last_maintenance_offset_days": -25,
|
||||
"install_date_offset_days": -1000
|
||||
},
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000015",
|
||||
"name": "Túnel de Congelación IQF",
|
||||
"type": "freezer",
|
||||
"model": "GEA ColdSteam CF-2000",
|
||||
"serial_number": "GEA-2021-5567",
|
||||
"location": "Zona de Congelación",
|
||||
"status": "operational",
|
||||
"power_kw": 180.0,
|
||||
"capacity": 2000.0,
|
||||
"efficiency_percentage": 91.0,
|
||||
"current_temperature": -45.0,
|
||||
"target_temperature": -45.0,
|
||||
"maintenance_interval_days": 90,
|
||||
"last_maintenance_offset_days": -40,
|
||||
"install_date_offset_days": -1460
|
||||
},
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000016",
|
||||
"name": "Línea de Empaquetado Automática",
|
||||
"type": "packaging",
|
||||
"model": "Bosch SVE 3600",
|
||||
"serial_number": "BO-2023-2234",
|
||||
"location": "Zona de Empaquetado",
|
||||
"status": "maintenance",
|
||||
"power_kw": 35.0,
|
||||
"capacity": 3600.0,
|
||||
"efficiency_percentage": 88.0,
|
||||
"maintenance_interval_days": 30,
|
||||
"last_maintenance_offset_days": -5,
|
||||
"install_date_offset_days": -300,
|
||||
"notes": "En mantenimiento programado. Retorno previsto en 2 días."
|
||||
},
|
||||
{
|
||||
"id": "30000000-0000-0000-0000-000000000017",
|
||||
"name": "Cámara de Fermentación Industrial 2",
|
||||
"type": "proofer",
|
||||
"model": "Sveba Dahlen FC-600",
|
||||
"serial_number": "SD-2020-2211",
|
||||
"location": "Zona de Fermentación",
|
||||
"status": "operational",
|
||||
"power_kw": 35.0,
|
||||
"capacity": 600.0,
|
||||
"efficiency_percentage": 89.0,
|
||||
"current_temperature": 28.0,
|
||||
"target_temperature": 28.0,
|
||||
"maintenance_interval_days": 60,
|
||||
"last_maintenance_offset_days": -35,
|
||||
"install_date_offset_days": -1825
|
||||
}
|
||||
]
|
||||
}
|
||||
545
services/production/scripts/demo/lotes_produccion_es.json
Normal file
545
services/production/scripts/demo/lotes_produccion_es.json
Normal file
@@ -0,0 +1,545 @@
|
||||
{
|
||||
"lotes_produccion": [
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "BATCH-20250115-001",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"product_name": "Baguette Francesa Tradicional",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000001",
|
||||
"planned_start_offset_days": -7,
|
||||
"planned_start_hour": 6,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 165,
|
||||
"planned_quantity": 100.0,
|
||||
"actual_quantity": 98.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "MEDIUM",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 98.0,
|
||||
"quality_score": 95.0,
|
||||
"waste_quantity": 2.0,
|
||||
"defect_quantity": 0.0,
|
||||
"estimated_cost": 150.00,
|
||||
"actual_cost": 148.50,
|
||||
"labor_cost": 80.00,
|
||||
"material_cost": 55.00,
|
||||
"overhead_cost": 13.50,
|
||||
"station_id": "STATION-01",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Producción estándar, sin incidencias",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000002",
|
||||
"batch_number": "BATCH-20250115-002",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"product_name": "Croissant de Mantequilla Artesanal",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000002",
|
||||
"planned_start_offset_days": -7,
|
||||
"planned_start_hour": 5,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 240,
|
||||
"planned_quantity": 120.0,
|
||||
"actual_quantity": 115.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "HIGH",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 95.8,
|
||||
"quality_score": 92.0,
|
||||
"waste_quantity": 3.0,
|
||||
"defect_quantity": 2.0,
|
||||
"estimated_cost": 280.00,
|
||||
"actual_cost": 275.00,
|
||||
"labor_cost": 120.00,
|
||||
"material_cost": 125.00,
|
||||
"overhead_cost": 30.00,
|
||||
"station_id": "STATION-02",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Laminado perfecto, buen desarrollo",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000002", "50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000003",
|
||||
"batch_number": "BATCH-20250116-001",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"product_name": "Pan de Pueblo con Masa Madre",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000003",
|
||||
"planned_start_offset_days": -6,
|
||||
"planned_start_hour": 7,
|
||||
"planned_start_minute": 30,
|
||||
"planned_duration_minutes": 300,
|
||||
"planned_quantity": 80.0,
|
||||
"actual_quantity": 80.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "MEDIUM",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 100.0,
|
||||
"quality_score": 98.0,
|
||||
"waste_quantity": 0.0,
|
||||
"defect_quantity": 0.0,
|
||||
"estimated_cost": 200.00,
|
||||
"actual_cost": 195.00,
|
||||
"labor_cost": 90.00,
|
||||
"material_cost": 80.00,
|
||||
"overhead_cost": 25.00,
|
||||
"station_id": "STATION-01",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": true,
|
||||
"production_notes": "Excelente fermentación de la masa madre",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000004",
|
||||
"batch_number": "BATCH-20250116-002",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"product_name": "Napolitana de Chocolate",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000004",
|
||||
"planned_start_offset_days": -6,
|
||||
"planned_start_hour": 6,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 180,
|
||||
"planned_quantity": 90.0,
|
||||
"actual_quantity": 88.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "MEDIUM",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 97.8,
|
||||
"quality_score": 94.0,
|
||||
"waste_quantity": 1.0,
|
||||
"defect_quantity": 1.0,
|
||||
"estimated_cost": 220.00,
|
||||
"actual_cost": 218.00,
|
||||
"labor_cost": 95.00,
|
||||
"material_cost": 98.00,
|
||||
"overhead_cost": 25.00,
|
||||
"station_id": "STATION-02",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Distribución uniforme del chocolate",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001", "50000000-0000-0000-0000-000000000002"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000005",
|
||||
"batch_number": "BATCH-20250117-001",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"product_name": "Baguette Francesa Tradicional",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000001",
|
||||
"planned_start_offset_days": -5,
|
||||
"planned_start_hour": 6,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 165,
|
||||
"planned_quantity": 120.0,
|
||||
"actual_quantity": 118.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "HIGH",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 98.3,
|
||||
"quality_score": 96.0,
|
||||
"waste_quantity": 1.5,
|
||||
"defect_quantity": 0.5,
|
||||
"estimated_cost": 180.00,
|
||||
"actual_cost": 177.00,
|
||||
"labor_cost": 95.00,
|
||||
"material_cost": 65.00,
|
||||
"overhead_cost": 17.00,
|
||||
"station_id": "STATION-01",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Lote grande para pedido especial",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000006",
|
||||
"batch_number": "BATCH-20250117-002",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"product_name": "Croissant de Mantequilla Artesanal",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000002",
|
||||
"planned_start_offset_days": -5,
|
||||
"planned_start_hour": 5,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 240,
|
||||
"planned_quantity": 100.0,
|
||||
"actual_quantity": 96.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "MEDIUM",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 96.0,
|
||||
"quality_score": 90.0,
|
||||
"waste_quantity": 2.0,
|
||||
"defect_quantity": 2.0,
|
||||
"estimated_cost": 240.00,
|
||||
"actual_cost": 238.00,
|
||||
"labor_cost": 105.00,
|
||||
"material_cost": 105.00,
|
||||
"overhead_cost": 28.00,
|
||||
"station_id": "STATION-02",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Algunos croissants con desarrollo irregular",
|
||||
"quality_notes": "Revisar temperatura de fermentación",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000002", "50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000007",
|
||||
"batch_number": "BATCH-20250118-001",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"product_name": "Baguette Francesa Tradicional",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000001",
|
||||
"planned_start_offset_days": -4,
|
||||
"planned_start_hour": 6,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 165,
|
||||
"planned_quantity": 100.0,
|
||||
"actual_quantity": 99.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "MEDIUM",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 99.0,
|
||||
"quality_score": 97.0,
|
||||
"waste_quantity": 1.0,
|
||||
"defect_quantity": 0.0,
|
||||
"estimated_cost": 150.00,
|
||||
"actual_cost": 149.00,
|
||||
"labor_cost": 80.00,
|
||||
"material_cost": 55.00,
|
||||
"overhead_cost": 14.00,
|
||||
"station_id": "STATION-01",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Excelente resultado",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000008",
|
||||
"batch_number": "BATCH-20250118-002",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"product_name": "Pan de Pueblo con Masa Madre",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000003",
|
||||
"planned_start_offset_days": -4,
|
||||
"planned_start_hour": 7,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 300,
|
||||
"planned_quantity": 60.0,
|
||||
"actual_quantity": 60.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "LOW",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 100.0,
|
||||
"quality_score": 99.0,
|
||||
"waste_quantity": 0.0,
|
||||
"defect_quantity": 0.0,
|
||||
"estimated_cost": 155.00,
|
||||
"actual_cost": 152.00,
|
||||
"labor_cost": 70.00,
|
||||
"material_cost": 65.00,
|
||||
"overhead_cost": 17.00,
|
||||
"station_id": "STATION-01",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": true,
|
||||
"production_notes": "Masa madre en punto óptimo",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000009",
|
||||
"batch_number": "BATCH-20250119-001",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"product_name": "Croissant de Mantequilla Artesanal",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000002",
|
||||
"planned_start_offset_days": -3,
|
||||
"planned_start_hour": 5,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 240,
|
||||
"planned_quantity": 150.0,
|
||||
"actual_quantity": 145.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "URGENT",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 96.7,
|
||||
"quality_score": 93.0,
|
||||
"waste_quantity": 3.0,
|
||||
"defect_quantity": 2.0,
|
||||
"estimated_cost": 350.00,
|
||||
"actual_cost": 345.00,
|
||||
"labor_cost": 150.00,
|
||||
"material_cost": 155.00,
|
||||
"overhead_cost": 40.00,
|
||||
"station_id": "STATION-02",
|
||||
"is_rush_order": true,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Pedido urgente de evento corporativo",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000002", "50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000010",
|
||||
"batch_number": "BATCH-20250119-002",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"product_name": "Napolitana de Chocolate",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000004",
|
||||
"planned_start_offset_days": -3,
|
||||
"planned_start_hour": 6,
|
||||
"planned_start_minute": 30,
|
||||
"planned_duration_minutes": 180,
|
||||
"planned_quantity": 80.0,
|
||||
"actual_quantity": 79.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "MEDIUM",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 98.8,
|
||||
"quality_score": 95.0,
|
||||
"waste_quantity": 0.5,
|
||||
"defect_quantity": 0.5,
|
||||
"estimated_cost": 195.00,
|
||||
"actual_cost": 192.00,
|
||||
"labor_cost": 85.00,
|
||||
"material_cost": 85.00,
|
||||
"overhead_cost": 22.00,
|
||||
"station_id": "STATION-02",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Buen resultado general",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000011",
|
||||
"batch_number": "BATCH-20250120-001",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"product_name": "Baguette Francesa Tradicional",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000001",
|
||||
"planned_start_offset_days": -2,
|
||||
"planned_start_hour": 6,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 165,
|
||||
"planned_quantity": 110.0,
|
||||
"actual_quantity": 108.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "MEDIUM",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 98.2,
|
||||
"quality_score": 96.0,
|
||||
"waste_quantity": 1.5,
|
||||
"defect_quantity": 0.5,
|
||||
"estimated_cost": 165.00,
|
||||
"actual_cost": 162.00,
|
||||
"labor_cost": 88.00,
|
||||
"material_cost": 60.00,
|
||||
"overhead_cost": 14.00,
|
||||
"station_id": "STATION-01",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Producción estándar",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000012",
|
||||
"batch_number": "BATCH-20250120-002",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"product_name": "Pan de Pueblo con Masa Madre",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000003",
|
||||
"planned_start_offset_days": -2,
|
||||
"planned_start_hour": 7,
|
||||
"planned_start_minute": 30,
|
||||
"planned_duration_minutes": 300,
|
||||
"planned_quantity": 70.0,
|
||||
"actual_quantity": 70.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "MEDIUM",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 100.0,
|
||||
"quality_score": 98.0,
|
||||
"waste_quantity": 0.0,
|
||||
"defect_quantity": 0.0,
|
||||
"estimated_cost": 175.00,
|
||||
"actual_cost": 172.00,
|
||||
"labor_cost": 80.00,
|
||||
"material_cost": 72.00,
|
||||
"overhead_cost": 20.00,
|
||||
"station_id": "STATION-01",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": true,
|
||||
"production_notes": "Fermentación perfecta",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000013",
|
||||
"batch_number": "BATCH-20250121-001",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"product_name": "Croissant de Mantequilla Artesanal",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000002",
|
||||
"planned_start_offset_days": -1,
|
||||
"planned_start_hour": 5,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 240,
|
||||
"planned_quantity": 130.0,
|
||||
"actual_quantity": 125.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "HIGH",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 96.2,
|
||||
"quality_score": 94.0,
|
||||
"waste_quantity": 3.0,
|
||||
"defect_quantity": 2.0,
|
||||
"estimated_cost": 310.00,
|
||||
"actual_cost": 305.00,
|
||||
"labor_cost": 135.00,
|
||||
"material_cost": 138.00,
|
||||
"overhead_cost": 32.00,
|
||||
"station_id": "STATION-02",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Demanda elevada del fin de semana",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000002", "50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000014",
|
||||
"batch_number": "BATCH-20250121-002",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"product_name": "Baguette Francesa Tradicional",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000001",
|
||||
"planned_start_offset_days": -1,
|
||||
"planned_start_hour": 6,
|
||||
"planned_start_minute": 30,
|
||||
"planned_duration_minutes": 165,
|
||||
"planned_quantity": 120.0,
|
||||
"actual_quantity": 118.0,
|
||||
"status": "COMPLETED",
|
||||
"priority": "HIGH",
|
||||
"current_process_stage": "packaging",
|
||||
"yield_percentage": 98.3,
|
||||
"quality_score": 97.0,
|
||||
"waste_quantity": 1.5,
|
||||
"defect_quantity": 0.5,
|
||||
"estimated_cost": 180.00,
|
||||
"actual_cost": 178.00,
|
||||
"labor_cost": 95.00,
|
||||
"material_cost": 66.00,
|
||||
"overhead_cost": 17.00,
|
||||
"station_id": "STATION-01",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Alta demanda de fin de semana",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000015",
|
||||
"batch_number": "BATCH-20250122-001",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"product_name": "Baguette Francesa Tradicional",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000001",
|
||||
"planned_start_offset_days": 0,
|
||||
"planned_start_hour": 6,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 165,
|
||||
"planned_quantity": 100.0,
|
||||
"actual_quantity": null,
|
||||
"status": "IN_PROGRESS",
|
||||
"priority": "MEDIUM",
|
||||
"current_process_stage": "baking",
|
||||
"yield_percentage": null,
|
||||
"quality_score": null,
|
||||
"waste_quantity": null,
|
||||
"defect_quantity": null,
|
||||
"estimated_cost": 150.00,
|
||||
"actual_cost": null,
|
||||
"labor_cost": null,
|
||||
"material_cost": null,
|
||||
"overhead_cost": null,
|
||||
"station_id": "STATION-01",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Producción en curso",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000016",
|
||||
"batch_number": "BATCH-20250122-002",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"product_name": "Croissant de Mantequilla Artesanal",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000002",
|
||||
"planned_start_offset_days": 0,
|
||||
"planned_start_hour": 8,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 240,
|
||||
"planned_quantity": 100.0,
|
||||
"actual_quantity": null,
|
||||
"status": "PENDING",
|
||||
"priority": "MEDIUM",
|
||||
"current_process_stage": null,
|
||||
"yield_percentage": null,
|
||||
"quality_score": null,
|
||||
"waste_quantity": null,
|
||||
"defect_quantity": null,
|
||||
"estimated_cost": 240.00,
|
||||
"actual_cost": null,
|
||||
"labor_cost": null,
|
||||
"material_cost": null,
|
||||
"overhead_cost": null,
|
||||
"station_id": "STATION-02",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Pendiente de inicio",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000002", "50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000017",
|
||||
"batch_number": "BATCH-20250123-001",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"product_name": "Pan de Pueblo con Masa Madre",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000003",
|
||||
"planned_start_offset_days": 1,
|
||||
"planned_start_hour": 7,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 300,
|
||||
"planned_quantity": 75.0,
|
||||
"actual_quantity": null,
|
||||
"status": "PENDING",
|
||||
"priority": "MEDIUM",
|
||||
"current_process_stage": null,
|
||||
"yield_percentage": null,
|
||||
"quality_score": null,
|
||||
"waste_quantity": null,
|
||||
"defect_quantity": null,
|
||||
"estimated_cost": 185.00,
|
||||
"actual_cost": null,
|
||||
"labor_cost": null,
|
||||
"material_cost": null,
|
||||
"overhead_cost": null,
|
||||
"station_id": "STATION-01",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": true,
|
||||
"production_notes": "Planificado para mañana",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000018",
|
||||
"batch_number": "BATCH-20250123-002",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"product_name": "Napolitana de Chocolate",
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000004",
|
||||
"planned_start_offset_days": 1,
|
||||
"planned_start_hour": 6,
|
||||
"planned_start_minute": 0,
|
||||
"planned_duration_minutes": 180,
|
||||
"planned_quantity": 85.0,
|
||||
"actual_quantity": null,
|
||||
"status": "PENDING",
|
||||
"priority": "LOW",
|
||||
"current_process_stage": null,
|
||||
"yield_percentage": null,
|
||||
"quality_score": null,
|
||||
"waste_quantity": null,
|
||||
"defect_quantity": null,
|
||||
"estimated_cost": 210.00,
|
||||
"actual_cost": null,
|
||||
"labor_cost": null,
|
||||
"material_cost": null,
|
||||
"overhead_cost": null,
|
||||
"station_id": "STATION-02",
|
||||
"is_rush_order": false,
|
||||
"is_special_recipe": false,
|
||||
"production_notes": "Planificado para mañana",
|
||||
"equipment_used": ["50000000-0000-0000-0000-000000000001"]
|
||||
}
|
||||
]
|
||||
}
|
||||
444
services/production/scripts/demo/plantillas_calidad_es.json
Normal file
444
services/production/scripts/demo/plantillas_calidad_es.json
Normal file
@@ -0,0 +1,444 @@
|
||||
{
|
||||
"plantillas_calidad": [
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000001",
|
||||
"name": "Control de Peso de Pan",
|
||||
"template_code": "QC-PESO-PAN-001",
|
||||
"check_type": "measurement",
|
||||
"category": "weight_check",
|
||||
"description": "Verificación del peso del pan después del horneado para asegurar cumplimiento con estándares",
|
||||
"instructions": "1. Seleccionar 5 panes de forma aleatoria del lote\n2. Pesar cada pan en balanza calibrada\n3. Calcular el peso promedio\n4. Verificar que está dentro de tolerancia\n5. Registrar resultados",
|
||||
"parameters": {
|
||||
"sample_size": 5,
|
||||
"unit": "gramos",
|
||||
"measurement_method": "balanza_digital"
|
||||
},
|
||||
"thresholds": {
|
||||
"min_acceptable": 95.0,
|
||||
"max_acceptable": 105.0,
|
||||
"target": 100.0
|
||||
},
|
||||
"scoring_criteria": {
|
||||
"excellent": {"min": 98.0, "max": 102.0},
|
||||
"good": {"min": 96.0, "max": 104.0},
|
||||
"acceptable": {"min": 95.0, "max": 105.0},
|
||||
"fail": {"below": 95.0, "above": 105.0}
|
||||
},
|
||||
"is_active": true,
|
||||
"is_required": true,
|
||||
"is_critical": true,
|
||||
"weight": 1.0,
|
||||
"min_value": 95.0,
|
||||
"max_value": 105.0,
|
||||
"target_value": 100.0,
|
||||
"unit": "g",
|
||||
"tolerance_percentage": 5.0,
|
||||
"applicable_stages": ["baking", "packaging"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000002",
|
||||
"name": "Control de Temperatura de Masa",
|
||||
"template_code": "QC-TEMP-MASA-001",
|
||||
"check_type": "measurement",
|
||||
"category": "temperature_check",
|
||||
"description": "Verificación de la temperatura de la masa durante el amasado",
|
||||
"instructions": "1. Insertar termómetro en el centro de la masa\n2. Esperar 30 segundos para lectura estable\n3. Registrar temperatura\n4. Verificar contra rango objetivo\n5. Ajustar velocidad o tiempo si necesario",
|
||||
"parameters": {
|
||||
"measurement_point": "centro_masa",
|
||||
"wait_time_seconds": 30,
|
||||
"unit": "celsius"
|
||||
},
|
||||
"thresholds": {
|
||||
"min_acceptable": 24.0,
|
||||
"max_acceptable": 27.0,
|
||||
"target": 25.5
|
||||
},
|
||||
"scoring_criteria": {
|
||||
"excellent": {"min": 25.0, "max": 26.0},
|
||||
"good": {"min": 24.5, "max": 26.5},
|
||||
"acceptable": {"min": 24.0, "max": 27.0},
|
||||
"fail": {"below": 24.0, "above": 27.0}
|
||||
},
|
||||
"is_active": true,
|
||||
"is_required": true,
|
||||
"is_critical": true,
|
||||
"weight": 1.0,
|
||||
"min_value": 24.0,
|
||||
"max_value": 27.0,
|
||||
"target_value": 25.5,
|
||||
"unit": "°C",
|
||||
"tolerance_percentage": 4.0,
|
||||
"applicable_stages": ["mixing"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000003",
|
||||
"name": "Inspección Visual de Color",
|
||||
"template_code": "QC-COLOR-001",
|
||||
"check_type": "visual",
|
||||
"category": "appearance",
|
||||
"description": "Evaluación del color del producto horneado usando escala de referencia",
|
||||
"instructions": "1. Comparar producto con carta de colores estándar\n2. Verificar uniformidad del dorado\n3. Buscar zonas pálidas o quemadas\n4. Calificar según escala\n5. Rechazar si fuera de tolerancia",
|
||||
"parameters": {
|
||||
"reference_standard": "Carta Munsell Panadería",
|
||||
"lighting": "luz_natural_6500K",
|
||||
"viewing_angle": "45_grados"
|
||||
},
|
||||
"thresholds": {
|
||||
"min_score": 7.0,
|
||||
"target_score": 9.0
|
||||
},
|
||||
"scoring_criteria": {
|
||||
"excellent": {"description": "Dorado uniforme, sin manchas", "score": 9.0},
|
||||
"good": {"description": "Buen color general, mínimas variaciones", "score": 8.0},
|
||||
"acceptable": {"description": "Color aceptable, ligeras irregularidades", "score": 7.0},
|
||||
"fail": {"description": "Pálido, quemado o muy irregular", "score": 6.0}
|
||||
},
|
||||
"is_active": true,
|
||||
"is_required": true,
|
||||
"is_critical": false,
|
||||
"weight": 0.8,
|
||||
"min_value": 7.0,
|
||||
"max_value": 10.0,
|
||||
"target_value": 9.0,
|
||||
"unit": "score",
|
||||
"tolerance_percentage": null,
|
||||
"applicable_stages": ["baking", "finishing"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000004",
|
||||
"name": "Control de Humedad de Miga",
|
||||
"template_code": "QC-HUMEDAD-001",
|
||||
"check_type": "measurement",
|
||||
"category": "moisture_check",
|
||||
"description": "Medición del porcentaje de humedad en la miga del pan",
|
||||
"instructions": "1. Cortar muestra del centro del pan\n2. Pesar muestra (peso húmedo)\n3. Secar en horno a 105°C durante 3 horas\n4. Pesar muestra seca\n5. Calcular porcentaje de humedad",
|
||||
"parameters": {
|
||||
"sample_weight_g": 10.0,
|
||||
"drying_temp_c": 105.0,
|
||||
"drying_time_hours": 3.0,
|
||||
"calculation": "(peso_húmedo - peso_seco) / peso_húmedo * 100"
|
||||
},
|
||||
"thresholds": {
|
||||
"min_acceptable": 35.0,
|
||||
"max_acceptable": 42.0,
|
||||
"target": 38.0
|
||||
},
|
||||
"scoring_criteria": {
|
||||
"excellent": {"min": 37.0, "max": 39.0},
|
||||
"good": {"min": 36.0, "max": 40.0},
|
||||
"acceptable": {"min": 35.0, "max": 42.0},
|
||||
"fail": {"below": 35.0, "above": 42.0}
|
||||
},
|
||||
"is_active": true,
|
||||
"is_required": false,
|
||||
"is_critical": false,
|
||||
"weight": 0.7,
|
||||
"min_value": 35.0,
|
||||
"max_value": 42.0,
|
||||
"target_value": 38.0,
|
||||
"unit": "%",
|
||||
"tolerance_percentage": 8.0,
|
||||
"applicable_stages": ["cooling", "finishing"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000005",
|
||||
"name": "Control de Volumen Específico",
|
||||
"template_code": "QC-VOLUMEN-001",
|
||||
"check_type": "measurement",
|
||||
"category": "volume_check",
|
||||
"description": "Medición del volumen específico del pan (cm³/g) para evaluar calidad de fermentación",
|
||||
"instructions": "1. Pesar el pan completo\n2. Medir volumen por desplazamiento de semillas\n3. Calcular volumen específico (volumen/peso)\n4. Comparar con estándar de producto\n5. Registrar resultado",
|
||||
"parameters": {
|
||||
"measurement_method": "desplazamiento_semillas",
|
||||
"medium": "semillas_colza",
|
||||
"calculation": "volumen_cm3 / peso_g"
|
||||
},
|
||||
"thresholds": {
|
||||
"min_acceptable": 3.5,
|
||||
"max_acceptable": 5.0,
|
||||
"target": 4.2
|
||||
},
|
||||
"scoring_criteria": {
|
||||
"excellent": {"min": 4.0, "max": 4.5},
|
||||
"good": {"min": 3.8, "max": 4.7},
|
||||
"acceptable": {"min": 3.5, "max": 5.0},
|
||||
"fail": {"below": 3.5, "above": 5.0}
|
||||
},
|
||||
"is_active": true,
|
||||
"is_required": false,
|
||||
"is_critical": false,
|
||||
"weight": 0.6,
|
||||
"min_value": 3.5,
|
||||
"max_value": 5.0,
|
||||
"target_value": 4.2,
|
||||
"unit": "cm³/g",
|
||||
"tolerance_percentage": 15.0,
|
||||
"applicable_stages": ["cooling", "finishing"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000006",
|
||||
"name": "Inspección de Corteza",
|
||||
"template_code": "QC-CORTEZA-001",
|
||||
"check_type": "visual",
|
||||
"category": "appearance",
|
||||
"description": "Evaluación visual de la calidad de la corteza del pan",
|
||||
"instructions": "1. Inspeccionar superficie completa del pan\n2. Verificar ausencia de grietas no deseadas\n3. Evaluar brillo y textura\n4. Verificar expansión de cortes\n5. Calificar integridad general",
|
||||
"parameters": {
|
||||
"inspection_points": ["grietas", "brillo", "textura", "cortes", "burbujas"]
|
||||
},
|
||||
"thresholds": {
|
||||
"min_score": 7.0,
|
||||
"target_score": 9.0
|
||||
},
|
||||
"scoring_criteria": {
|
||||
"excellent": {"description": "Corteza perfecta, cortes bien expandidos, sin defectos", "score": 9.0},
|
||||
"good": {"description": "Corteza buena, mínimos defectos superficiales", "score": 8.0},
|
||||
"acceptable": {"description": "Corteza aceptable, algunos defectos menores", "score": 7.0},
|
||||
"fail": {"description": "Grietas excesivas, corteza rota o muy irregular", "score": 6.0}
|
||||
},
|
||||
"is_active": true,
|
||||
"is_required": true,
|
||||
"is_critical": false,
|
||||
"weight": 0.8,
|
||||
"min_value": 7.0,
|
||||
"max_value": 10.0,
|
||||
"target_value": 9.0,
|
||||
"unit": "score",
|
||||
"tolerance_percentage": null,
|
||||
"applicable_stages": ["cooling", "finishing"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000007",
|
||||
"name": "Control de Temperatura de Horneado",
|
||||
"template_code": "QC-TEMP-HORNO-001",
|
||||
"check_type": "measurement",
|
||||
"category": "temperature_check",
|
||||
"description": "Verificación de la temperatura del horno durante el horneado",
|
||||
"instructions": "1. Verificar temperatura en termómetro de horno\n2. Confirmar con termómetro independiente\n3. Registrar temperatura cada 5 minutos\n4. Verificar estabilidad\n5. Ajustar si está fuera de rango",
|
||||
"parameters": {
|
||||
"measurement_frequency_minutes": 5,
|
||||
"measurement_points": ["superior", "central", "inferior"],
|
||||
"acceptable_variation": 5.0
|
||||
},
|
||||
"thresholds": {
|
||||
"min_acceptable": 210.0,
|
||||
"max_acceptable": 230.0,
|
||||
"target": 220.0
|
||||
},
|
||||
"scoring_criteria": {
|
||||
"excellent": {"min": 218.0, "max": 222.0},
|
||||
"good": {"min": 215.0, "max": 225.0},
|
||||
"acceptable": {"min": 210.0, "max": 230.0},
|
||||
"fail": {"below": 210.0, "above": 230.0}
|
||||
},
|
||||
"is_active": true,
|
||||
"is_required": true,
|
||||
"is_critical": true,
|
||||
"weight": 1.0,
|
||||
"min_value": 210.0,
|
||||
"max_value": 230.0,
|
||||
"target_value": 220.0,
|
||||
"unit": "°C",
|
||||
"tolerance_percentage": 2.5,
|
||||
"applicable_stages": ["baking"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000008",
|
||||
"name": "Control de Tiempo de Fermentación",
|
||||
"template_code": "QC-TIEMPO-FERM-001",
|
||||
"check_type": "measurement",
|
||||
"category": "time_check",
|
||||
"description": "Monitoreo del tiempo de fermentación y crecimiento de la masa",
|
||||
"instructions": "1. Marcar nivel inicial de masa en recipiente\n2. Iniciar cronómetro\n3. Monitorear crecimiento cada 15 minutos\n4. Verificar duplicación de volumen\n5. Registrar tiempo total",
|
||||
"parameters": {
|
||||
"target_growth": "duplicar_volumen",
|
||||
"monitoring_frequency_minutes": 15,
|
||||
"ambient_conditions": "temperatura_controlada"
|
||||
},
|
||||
"thresholds": {
|
||||
"min_acceptable": 45.0,
|
||||
"max_acceptable": 75.0,
|
||||
"target": 60.0
|
||||
},
|
||||
"scoring_criteria": {
|
||||
"excellent": {"min": 55.0, "max": 65.0},
|
||||
"good": {"min": 50.0, "max": 70.0},
|
||||
"acceptable": {"min": 45.0, "max": 75.0},
|
||||
"fail": {"below": 45.0, "above": 75.0}
|
||||
},
|
||||
"is_active": true,
|
||||
"is_required": true,
|
||||
"is_critical": true,
|
||||
"weight": 1.0,
|
||||
"min_value": 45.0,
|
||||
"max_value": 75.0,
|
||||
"target_value": 60.0,
|
||||
"unit": "minutos",
|
||||
"tolerance_percentage": 15.0,
|
||||
"applicable_stages": ["proofing"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000009",
|
||||
"name": "Inspección de Estructura de Miga",
|
||||
"template_code": "QC-MIGA-001",
|
||||
"check_type": "visual",
|
||||
"category": "texture",
|
||||
"description": "Evaluación de la estructura alveolar de la miga del pan",
|
||||
"instructions": "1. Cortar pan por la mitad longitudinalmente\n2. Observar distribución de alveolos\n3. Evaluar uniformidad del alveolado\n4. Verificar ausencia de grandes cavidades\n5. Evaluar elasticidad al tacto",
|
||||
"parameters": {
|
||||
"cutting_method": "longitudinal_centro",
|
||||
"evaluation_criteria": ["tamaño_alveolos", "distribución", "uniformidad", "elasticidad"]
|
||||
},
|
||||
"thresholds": {
|
||||
"min_score": 7.0,
|
||||
"target_score": 9.0
|
||||
},
|
||||
"scoring_criteria": {
|
||||
"excellent": {"description": "Alveolado fino y uniforme, miga elástica", "score": 9.0},
|
||||
"good": {"description": "Buen alveolado, ligeras variaciones", "score": 8.0},
|
||||
"acceptable": {"description": "Alveolado aceptable, algunas irregularidades", "score": 7.0},
|
||||
"fail": {"description": "Miga densa, grandes cavidades o muy irregular", "score": 6.0}
|
||||
},
|
||||
"is_active": true,
|
||||
"is_required": true,
|
||||
"is_critical": false,
|
||||
"weight": 0.8,
|
||||
"min_value": 7.0,
|
||||
"max_value": 10.0,
|
||||
"target_value": 9.0,
|
||||
"unit": "score",
|
||||
"tolerance_percentage": null,
|
||||
"applicable_stages": ["finishing"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000010",
|
||||
"name": "Control de pH de Masa",
|
||||
"template_code": "QC-PH-MASA-001",
|
||||
"check_type": "measurement",
|
||||
"category": "chemical",
|
||||
"description": "Medición del pH de la masa para verificar acidez correcta",
|
||||
"instructions": "1. Tomar muestra de 10g de masa\n2. Diluir en 90ml de agua destilada\n3. Homogeneizar bien\n4. Medir pH con peachímetro calibrado\n5. Registrar lectura",
|
||||
"parameters": {
|
||||
"sample_size_g": 10.0,
|
||||
"dilution_ml": 90.0,
|
||||
"calibration_required": true,
|
||||
"measurement_temp_c": 20.0
|
||||
},
|
||||
"thresholds": {
|
||||
"min_acceptable": 5.0,
|
||||
"max_acceptable": 5.8,
|
||||
"target": 5.4
|
||||
},
|
||||
"scoring_criteria": {
|
||||
"excellent": {"min": 5.3, "max": 5.5},
|
||||
"good": {"min": 5.2, "max": 5.6},
|
||||
"acceptable": {"min": 5.0, "max": 5.8},
|
||||
"fail": {"below": 5.0, "above": 5.8}
|
||||
},
|
||||
"is_active": true,
|
||||
"is_required": false,
|
||||
"is_critical": false,
|
||||
"weight": 0.5,
|
||||
"min_value": 5.0,
|
||||
"max_value": 5.8,
|
||||
"target_value": 5.4,
|
||||
"unit": "pH",
|
||||
"tolerance_percentage": 5.0,
|
||||
"applicable_stages": ["mixing", "proofing"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000011",
|
||||
"name": "Control de Dimensiones",
|
||||
"template_code": "QC-DIM-001",
|
||||
"check_type": "measurement",
|
||||
"category": "dimensions",
|
||||
"description": "Verificación de las dimensiones del producto terminado",
|
||||
"instructions": "1. Medir largo con cinta métrica\n2. Medir ancho en punto más amplio\n3. Medir altura en punto máximo\n4. Verificar contra especificaciones\n5. Calcular promedio de 3 muestras",
|
||||
"parameters": {
|
||||
"sample_size": 3,
|
||||
"measurement_tool": "calibre_digital",
|
||||
"precision_mm": 1.0
|
||||
},
|
||||
"thresholds": {
|
||||
"length_mm": {"min": 280.0, "max": 320.0, "target": 300.0},
|
||||
"width_mm": {"min": 140.0, "max": 160.0, "target": 150.0},
|
||||
"height_mm": {"min": 90.0, "max": 110.0, "target": 100.0}
|
||||
},
|
||||
"scoring_criteria": {
|
||||
"excellent": {"description": "Todas las dimensiones dentro de ±3%", "score": 9.0},
|
||||
"good": {"description": "Dimensiones dentro de ±5%", "score": 8.0},
|
||||
"acceptable": {"description": "Dimensiones dentro de tolerancia", "score": 7.0},
|
||||
"fail": {"description": "Una o más dimensiones fuera de tolerancia", "score": 6.0}
|
||||
},
|
||||
"is_active": true,
|
||||
"is_required": true,
|
||||
"is_critical": false,
|
||||
"weight": 0.7,
|
||||
"min_value": 7.0,
|
||||
"max_value": 10.0,
|
||||
"target_value": 9.0,
|
||||
"unit": "score",
|
||||
"tolerance_percentage": 5.0,
|
||||
"applicable_stages": ["packaging", "finishing"]
|
||||
},
|
||||
{
|
||||
"id": "40000000-0000-0000-0000-000000000012",
|
||||
"name": "Inspección de Higiene y Limpieza",
|
||||
"template_code": "QC-HIGIENE-001",
|
||||
"check_type": "checklist",
|
||||
"category": "hygiene",
|
||||
"description": "Verificación de condiciones higiénicas durante la producción",
|
||||
"instructions": "1. Verificar limpieza de superficies de trabajo\n2. Inspeccionar vestimenta del personal\n3. Verificar lavado de manos\n4. Revisar limpieza de equipos\n5. Completar checklist",
|
||||
"parameters": {
|
||||
"checklist_items": [
|
||||
"superficies_limpias",
|
||||
"uniformes_limpios",
|
||||
"manos_lavadas",
|
||||
"equipos_sanitizados",
|
||||
"ausencia_contaminantes",
|
||||
"temperatura_ambiente_correcta"
|
||||
]
|
||||
},
|
||||
"thresholds": {
|
||||
"min_items_passed": 5,
|
||||
"total_items": 6
|
||||
},
|
||||
"scoring_criteria": {
|
||||
"excellent": {"items_passed": 6, "description": "100% cumplimiento"},
|
||||
"good": {"items_passed": 5, "description": "Cumplimiento aceptable"},
|
||||
"fail": {"items_passed": 4, "description": "Incumplimiento inaceptable"}
|
||||
},
|
||||
"is_active": true,
|
||||
"is_required": true,
|
||||
"is_critical": true,
|
||||
"weight": 1.0,
|
||||
"min_value": 5.0,
|
||||
"max_value": 6.0,
|
||||
"target_value": 6.0,
|
||||
"unit": "items",
|
||||
"tolerance_percentage": null,
|
||||
"applicable_stages": ["mixing", "shaping", "baking", "packaging"]
|
||||
}
|
||||
],
|
||||
"notas": {
|
||||
"total_plantillas": 12,
|
||||
"distribucion": {
|
||||
"mediciones": 7,
|
||||
"visuales": 3,
|
||||
"checklist": 1,
|
||||
"quimicas": 1
|
||||
},
|
||||
"criticidad": {
|
||||
"criticas": 6,
|
||||
"no_criticas": 6
|
||||
},
|
||||
"etapas_aplicables": [
|
||||
"mixing",
|
||||
"proofing",
|
||||
"baking",
|
||||
"cooling",
|
||||
"packaging",
|
||||
"finishing"
|
||||
]
|
||||
}
|
||||
}
|
||||
302
services/production/scripts/demo/seed_demo_batches.py
Executable file
302
services/production/scripts/demo/seed_demo_batches.py
Executable file
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Demo Production Batches Seeding Script for Production Service
|
||||
Creates production batches for demo template tenants
|
||||
|
||||
This script runs as a Kubernetes init job inside the production-service container.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# Add app to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import select
|
||||
import structlog
|
||||
|
||||
from app.models.production import ProductionBatch, ProductionStatus, ProductionPriority, ProcessStage
|
||||
|
||||
# Configure logging
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Base demo tenant IDs
|
||||
DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Individual bakery
|
||||
DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # Central bakery
|
||||
|
||||
# Base reference date for date calculations
|
||||
BASE_REFERENCE_DATE = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def load_batches_data():
|
||||
"""Load production batches data from JSON file"""
|
||||
data_file = Path(__file__).parent / "lotes_produccion_es.json"
|
||||
if not data_file.exists():
|
||||
raise FileNotFoundError(f"Production batches data file not found: {data_file}")
|
||||
|
||||
with open(data_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def calculate_datetime_from_offset(offset_days: int, hour: int, minute: int) -> datetime:
|
||||
"""Calculate a datetime based on offset from BASE_REFERENCE_DATE"""
|
||||
base_date = BASE_REFERENCE_DATE.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
return base_date + timedelta(days=offset_days)
|
||||
|
||||
|
||||
def map_status(status_str: str) -> ProductionStatus:
|
||||
"""Map status string to enum"""
|
||||
mapping = {
|
||||
"PENDING": ProductionStatus.PENDING,
|
||||
"IN_PROGRESS": ProductionStatus.IN_PROGRESS,
|
||||
"COMPLETED": ProductionStatus.COMPLETED,
|
||||
"CANCELLED": ProductionStatus.CANCELLED,
|
||||
"ON_HOLD": ProductionStatus.ON_HOLD,
|
||||
"QUALITY_CHECK": ProductionStatus.QUALITY_CHECK,
|
||||
"FAILED": ProductionStatus.FAILED
|
||||
}
|
||||
return mapping.get(status_str, ProductionStatus.PENDING)
|
||||
|
||||
|
||||
def map_priority(priority_str: str) -> ProductionPriority:
|
||||
"""Map priority string to enum"""
|
||||
mapping = {
|
||||
"LOW": ProductionPriority.LOW,
|
||||
"MEDIUM": ProductionPriority.MEDIUM,
|
||||
"HIGH": ProductionPriority.HIGH,
|
||||
"URGENT": ProductionPriority.URGENT
|
||||
}
|
||||
return mapping.get(priority_str, ProductionPriority.MEDIUM)
|
||||
|
||||
|
||||
def map_process_stage(stage_str: str) -> ProcessStage:
|
||||
"""Map process stage string to enum"""
|
||||
if not stage_str:
|
||||
return None
|
||||
|
||||
mapping = {
|
||||
"mixing": ProcessStage.MIXING,
|
||||
"proofing": ProcessStage.PROOFING,
|
||||
"shaping": ProcessStage.SHAPING,
|
||||
"baking": ProcessStage.BAKING,
|
||||
"cooling": ProcessStage.COOLING,
|
||||
"packaging": ProcessStage.PACKAGING,
|
||||
"finishing": ProcessStage.FINISHING
|
||||
}
|
||||
return mapping.get(stage_str, None)
|
||||
|
||||
|
||||
async def seed_batches_for_tenant(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
tenant_name: str,
|
||||
batches_list: list
|
||||
):
|
||||
"""Seed production batches for a specific tenant"""
|
||||
logger.info(f"Seeding production batches for: {tenant_name}", tenant_id=str(tenant_id))
|
||||
|
||||
# Check if batches already exist
|
||||
result = await db.execute(
|
||||
select(ProductionBatch).where(ProductionBatch.tenant_id == tenant_id).limit(1)
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f"Production batches already exist for {tenant_name}, skipping seed")
|
||||
return {"tenant_id": str(tenant_id), "batches_created": 0, "skipped": True}
|
||||
|
||||
count = 0
|
||||
for batch_data in batches_list:
|
||||
# Calculate planned start and end times
|
||||
planned_start = calculate_datetime_from_offset(
|
||||
batch_data["planned_start_offset_days"],
|
||||
batch_data["planned_start_hour"],
|
||||
batch_data["planned_start_minute"]
|
||||
)
|
||||
|
||||
planned_end = planned_start + timedelta(minutes=batch_data["planned_duration_minutes"])
|
||||
|
||||
# Calculate actual times for completed batches
|
||||
actual_start = None
|
||||
actual_end = None
|
||||
completed_at = None
|
||||
actual_duration = None
|
||||
|
||||
if batch_data["status"] in ["COMPLETED", "QUALITY_CHECK"]:
|
||||
actual_start = planned_start # Assume started on time
|
||||
actual_duration = batch_data["planned_duration_minutes"]
|
||||
actual_end = actual_start + timedelta(minutes=actual_duration)
|
||||
completed_at = actual_end
|
||||
elif batch_data["status"] == "IN_PROGRESS":
|
||||
actual_start = planned_start
|
||||
actual_duration = None
|
||||
actual_end = None
|
||||
|
||||
# For San Pablo, use original IDs. For La Espiga, generate new UUIDs
|
||||
if tenant_id == DEMO_TENANT_SAN_PABLO:
|
||||
batch_id = uuid.UUID(batch_data["id"])
|
||||
else:
|
||||
# Generate deterministic UUID for La Espiga based on original ID
|
||||
base_uuid = uuid.UUID(batch_data["id"])
|
||||
# Add a fixed offset to create a unique but deterministic ID
|
||||
batch_id = uuid.UUID(int=base_uuid.int + 0x10000000000000000000000000000000)
|
||||
|
||||
# Map enums
|
||||
status = map_status(batch_data["status"])
|
||||
priority = map_priority(batch_data["priority"])
|
||||
current_stage = map_process_stage(batch_data.get("current_process_stage"))
|
||||
|
||||
# Create unique batch number for each tenant
|
||||
if tenant_id == DEMO_TENANT_SAN_PABLO:
|
||||
batch_number = batch_data["batch_number"]
|
||||
else:
|
||||
# For La Espiga, append tenant suffix to make batch number unique
|
||||
batch_number = batch_data["batch_number"] + "-LE"
|
||||
|
||||
# Create production batch
|
||||
batch = ProductionBatch(
|
||||
id=batch_id,
|
||||
tenant_id=tenant_id,
|
||||
batch_number=batch_number,
|
||||
product_id=uuid.UUID(batch_data["product_id"]),
|
||||
product_name=batch_data["product_name"],
|
||||
recipe_id=uuid.UUID(batch_data["recipe_id"]) if batch_data.get("recipe_id") else None,
|
||||
planned_start_time=planned_start,
|
||||
planned_end_time=planned_end,
|
||||
planned_quantity=batch_data["planned_quantity"],
|
||||
planned_duration_minutes=batch_data["planned_duration_minutes"],
|
||||
actual_start_time=actual_start,
|
||||
actual_end_time=actual_end,
|
||||
actual_quantity=batch_data.get("actual_quantity"),
|
||||
actual_duration_minutes=actual_duration,
|
||||
status=status,
|
||||
priority=priority,
|
||||
current_process_stage=current_stage,
|
||||
yield_percentage=batch_data.get("yield_percentage"),
|
||||
quality_score=batch_data.get("quality_score"),
|
||||
waste_quantity=batch_data.get("waste_quantity"),
|
||||
defect_quantity=batch_data.get("defect_quantity"),
|
||||
estimated_cost=batch_data.get("estimated_cost"),
|
||||
actual_cost=batch_data.get("actual_cost"),
|
||||
labor_cost=batch_data.get("labor_cost"),
|
||||
material_cost=batch_data.get("material_cost"),
|
||||
overhead_cost=batch_data.get("overhead_cost"),
|
||||
equipment_used=batch_data.get("equipment_used"),
|
||||
station_id=batch_data.get("station_id"),
|
||||
is_rush_order=batch_data.get("is_rush_order", False),
|
||||
is_special_recipe=batch_data.get("is_special_recipe", False),
|
||||
production_notes=batch_data.get("production_notes"),
|
||||
quality_notes=batch_data.get("quality_notes"),
|
||||
created_at=BASE_REFERENCE_DATE,
|
||||
updated_at=BASE_REFERENCE_DATE,
|
||||
completed_at=completed_at
|
||||
)
|
||||
|
||||
db.add(batch)
|
||||
count += 1
|
||||
logger.debug(f"Created production batch: {batch.batch_number}", batch_id=str(batch.id))
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Successfully created {count} production batches for {tenant_name}")
|
||||
|
||||
return {
|
||||
"tenant_id": str(tenant_id),
|
||||
"batches_created": count,
|
||||
"skipped": False
|
||||
}
|
||||
|
||||
|
||||
async def seed_all(db: AsyncSession):
|
||||
"""Seed all demo tenants with production batches"""
|
||||
logger.info("Starting demo production batches seed process")
|
||||
|
||||
# Load batches data
|
||||
data = load_batches_data()
|
||||
|
||||
results = []
|
||||
|
||||
# Both tenants get the same production batches
|
||||
result_san_pablo = await seed_batches_for_tenant(
|
||||
db,
|
||||
DEMO_TENANT_SAN_PABLO,
|
||||
"San Pablo - Individual Bakery",
|
||||
data["lotes_produccion"]
|
||||
)
|
||||
results.append(result_san_pablo)
|
||||
|
||||
result_la_espiga = await seed_batches_for_tenant(
|
||||
db,
|
||||
DEMO_TENANT_LA_ESPIGA,
|
||||
"La Espiga - Central Bakery",
|
||||
data["lotes_produccion"]
|
||||
)
|
||||
results.append(result_la_espiga)
|
||||
|
||||
total_created = sum(r["batches_created"] for r in results)
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"total_batches_created": total_created,
|
||||
"status": "completed"
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main execution function"""
|
||||
# Get database URL from environment
|
||||
database_url = os.getenv("PRODUCTION_DATABASE_URL")
|
||||
if not database_url:
|
||||
logger.error("PRODUCTION_DATABASE_URL environment variable must be set")
|
||||
return 1
|
||||
|
||||
# Ensure asyncpg driver
|
||||
if database_url.startswith("postgresql://"):
|
||||
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
|
||||
# Create async engine
|
||||
engine = create_async_engine(database_url, echo=False)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
try:
|
||||
async with async_session() as session:
|
||||
result = await seed_all(session)
|
||||
|
||||
logger.info(
|
||||
"Production batches seed completed successfully!",
|
||||
total_batches=result["total_batches_created"],
|
||||
status=result["status"]
|
||||
)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "="*60)
|
||||
print("DEMO PRODUCTION BATCHES SEED SUMMARY")
|
||||
print("="*60)
|
||||
for tenant_result in result["results"]:
|
||||
tenant_id = tenant_result["tenant_id"]
|
||||
count = tenant_result["batches_created"]
|
||||
skipped = tenant_result.get("skipped", False)
|
||||
status = "SKIPPED (already exists)" if skipped else f"CREATED {count} batches"
|
||||
print(f"Tenant {tenant_id}: {status}")
|
||||
print(f"\nTotal Batches Created: {result['total_batches_created']}")
|
||||
print("="*60 + "\n")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Production batches seed failed: {str(e)}", exc_info=True)
|
||||
return 1
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
235
services/production/scripts/demo/seed_demo_equipment.py
Executable file
235
services/production/scripts/demo/seed_demo_equipment.py
Executable file
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Demo Equipment Seeding Script for Production Service
|
||||
Creates production equipment for demo template tenants
|
||||
|
||||
This script runs as a Kubernetes init job inside the production-service container.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# Add app to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import select
|
||||
import structlog
|
||||
|
||||
from app.models.production import Equipment, EquipmentType, EquipmentStatus
|
||||
|
||||
# Configure logging
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Base demo tenant IDs
|
||||
DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Individual bakery
|
||||
DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # Central bakery
|
||||
|
||||
# Base reference date for date calculations
|
||||
BASE_REFERENCE_DATE = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def load_equipment_data():
|
||||
"""Load equipment data from JSON file"""
|
||||
data_file = Path(__file__).parent / "equipos_es.json"
|
||||
if not data_file.exists():
|
||||
raise FileNotFoundError(f"Equipment data file not found: {data_file}")
|
||||
|
||||
with open(data_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def calculate_date_from_offset(offset_days: int) -> datetime:
|
||||
"""Calculate a date based on offset from BASE_REFERENCE_DATE"""
|
||||
return BASE_REFERENCE_DATE + timedelta(days=offset_days)
|
||||
|
||||
|
||||
async def seed_equipment_for_tenant(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
tenant_name: str,
|
||||
equipment_list: list
|
||||
):
|
||||
"""Seed equipment for a specific tenant"""
|
||||
logger.info(f"Seeding equipment for: {tenant_name}", tenant_id=str(tenant_id))
|
||||
|
||||
# Check if equipment already exists
|
||||
result = await db.execute(
|
||||
select(Equipment).where(Equipment.tenant_id == tenant_id).limit(1)
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f"Equipment already exists for {tenant_name}, skipping seed")
|
||||
return {"tenant_id": str(tenant_id), "equipment_created": 0, "skipped": True}
|
||||
|
||||
count = 0
|
||||
for equip_data in equipment_list:
|
||||
# Calculate dates from offsets
|
||||
install_date = None
|
||||
if "install_date_offset_days" in equip_data:
|
||||
install_date = calculate_date_from_offset(equip_data["install_date_offset_days"])
|
||||
|
||||
last_maintenance_date = None
|
||||
if "last_maintenance_offset_days" in equip_data:
|
||||
last_maintenance_date = calculate_date_from_offset(equip_data["last_maintenance_offset_days"])
|
||||
|
||||
# Calculate next maintenance date
|
||||
next_maintenance_date = None
|
||||
if last_maintenance_date and equip_data.get("maintenance_interval_days"):
|
||||
next_maintenance_date = last_maintenance_date + timedelta(
|
||||
days=equip_data["maintenance_interval_days"]
|
||||
)
|
||||
|
||||
# Map status string to enum
|
||||
status_mapping = {
|
||||
"operational": EquipmentStatus.OPERATIONAL,
|
||||
"warning": EquipmentStatus.WARNING,
|
||||
"maintenance": EquipmentStatus.MAINTENANCE,
|
||||
"down": EquipmentStatus.DOWN
|
||||
}
|
||||
status = status_mapping.get(equip_data["status"], EquipmentStatus.OPERATIONAL)
|
||||
|
||||
# Map type string to enum
|
||||
type_mapping = {
|
||||
"oven": EquipmentType.OVEN,
|
||||
"mixer": EquipmentType.MIXER,
|
||||
"proofer": EquipmentType.PROOFER,
|
||||
"freezer": EquipmentType.FREEZER,
|
||||
"packaging": EquipmentType.PACKAGING,
|
||||
"other": EquipmentType.OTHER
|
||||
}
|
||||
equipment_type = type_mapping.get(equip_data["type"], EquipmentType.OTHER)
|
||||
|
||||
# Create equipment
|
||||
equipment = Equipment(
|
||||
id=uuid.UUID(equip_data["id"]),
|
||||
tenant_id=tenant_id,
|
||||
name=equip_data["name"],
|
||||
type=equipment_type,
|
||||
model=equip_data.get("model"),
|
||||
serial_number=equip_data.get("serial_number"),
|
||||
location=equip_data.get("location"),
|
||||
status=status,
|
||||
power_kw=equip_data.get("power_kw"),
|
||||
capacity=equip_data.get("capacity"),
|
||||
efficiency_percentage=equip_data.get("efficiency_percentage"),
|
||||
current_temperature=equip_data.get("current_temperature"),
|
||||
target_temperature=equip_data.get("target_temperature"),
|
||||
maintenance_interval_days=equip_data.get("maintenance_interval_days"),
|
||||
last_maintenance_date=last_maintenance_date,
|
||||
next_maintenance_date=next_maintenance_date,
|
||||
install_date=install_date,
|
||||
notes=equip_data.get("notes"),
|
||||
created_at=BASE_REFERENCE_DATE,
|
||||
updated_at=BASE_REFERENCE_DATE
|
||||
)
|
||||
|
||||
db.add(equipment)
|
||||
count += 1
|
||||
logger.debug(f"Created equipment: {equipment.name}", equipment_id=str(equipment.id))
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Successfully created {count} equipment items for {tenant_name}")
|
||||
|
||||
return {
|
||||
"tenant_id": str(tenant_id),
|
||||
"equipment_created": count,
|
||||
"skipped": False
|
||||
}
|
||||
|
||||
|
||||
async def seed_all(db: AsyncSession):
|
||||
"""Seed all demo tenants with equipment"""
|
||||
logger.info("Starting demo equipment seed process")
|
||||
|
||||
# Load equipment data
|
||||
data = load_equipment_data()
|
||||
|
||||
results = []
|
||||
|
||||
# Seed San Pablo (Individual Bakery)
|
||||
result_san_pablo = await seed_equipment_for_tenant(
|
||||
db,
|
||||
DEMO_TENANT_SAN_PABLO,
|
||||
"San Pablo - Individual Bakery",
|
||||
data["equipos_individual_bakery"]
|
||||
)
|
||||
results.append(result_san_pablo)
|
||||
|
||||
# Seed La Espiga (Central Bakery)
|
||||
result_la_espiga = await seed_equipment_for_tenant(
|
||||
db,
|
||||
DEMO_TENANT_LA_ESPIGA,
|
||||
"La Espiga - Central Bakery",
|
||||
data["equipos_central_bakery"]
|
||||
)
|
||||
results.append(result_la_espiga)
|
||||
|
||||
total_created = sum(r["equipment_created"] for r in results)
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"total_equipment_created": total_created,
|
||||
"status": "completed"
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main execution function"""
|
||||
# Get database URL from environment
|
||||
database_url = os.getenv("PRODUCTION_DATABASE_URL")
|
||||
if not database_url:
|
||||
logger.error("PRODUCTION_DATABASE_URL environment variable must be set")
|
||||
return 1
|
||||
|
||||
# Ensure asyncpg driver
|
||||
if database_url.startswith("postgresql://"):
|
||||
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
|
||||
# Create async engine
|
||||
engine = create_async_engine(database_url, echo=False)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
try:
|
||||
async with async_session() as session:
|
||||
result = await seed_all(session)
|
||||
|
||||
logger.info(
|
||||
"Equipment seed completed successfully!",
|
||||
total_equipment=result["total_equipment_created"],
|
||||
status=result["status"]
|
||||
)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "="*60)
|
||||
print("DEMO EQUIPMENT SEED SUMMARY")
|
||||
print("="*60)
|
||||
for tenant_result in result["results"]:
|
||||
tenant_id = tenant_result["tenant_id"]
|
||||
count = tenant_result["equipment_created"]
|
||||
skipped = tenant_result.get("skipped", False)
|
||||
status = "SKIPPED (already exists)" if skipped else f"CREATED {count} items"
|
||||
print(f"Tenant {tenant_id}: {status}")
|
||||
print(f"\nTotal Equipment Created: {result['total_equipment_created']}")
|
||||
print("="*60 + "\n")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Equipment seed failed: {str(e)}", exc_info=True)
|
||||
return 1
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
216
services/production/scripts/demo/seed_demo_quality_templates.py
Executable file
216
services/production/scripts/demo/seed_demo_quality_templates.py
Executable file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Demo Quality Templates Seeding Script for Production Service
|
||||
Creates quality check templates for demo template tenants
|
||||
|
||||
This script runs as a Kubernetes init job inside the production-service container.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# Add app to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import select
|
||||
import structlog
|
||||
|
||||
from app.models.production import QualityCheckTemplate
|
||||
|
||||
# Configure logging
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Base demo tenant IDs
|
||||
DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Individual bakery
|
||||
DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # Central bakery
|
||||
|
||||
# System user ID (first admin user from auth service)
|
||||
SYSTEM_USER_ID = uuid.UUID("30000000-0000-0000-0000-000000000001")
|
||||
|
||||
# Base reference date for date calculations
|
||||
BASE_REFERENCE_DATE = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def load_quality_templates_data():
|
||||
"""Load quality templates data from JSON file"""
|
||||
data_file = Path(__file__).parent / "plantillas_calidad_es.json"
|
||||
if not data_file.exists():
|
||||
raise FileNotFoundError(f"Quality templates data file not found: {data_file}")
|
||||
|
||||
with open(data_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
# Model uses simple strings, no need for enum mapping functions
|
||||
|
||||
|
||||
async def seed_quality_templates_for_tenant(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
tenant_name: str,
|
||||
templates_list: list
|
||||
):
|
||||
"""Seed quality templates for a specific tenant"""
|
||||
logger.info(f"Seeding quality templates for: {tenant_name}", tenant_id=str(tenant_id))
|
||||
|
||||
# Check if templates already exist
|
||||
result = await db.execute(
|
||||
select(QualityCheckTemplate).where(QualityCheckTemplate.tenant_id == tenant_id).limit(1)
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f"Quality templates already exist for {tenant_name}, skipping seed")
|
||||
return {"tenant_id": str(tenant_id), "templates_created": 0, "skipped": True}
|
||||
|
||||
count = 0
|
||||
for template_data in templates_list:
|
||||
# Use strings directly (model doesn't use enums)
|
||||
check_type = template_data["check_type"]
|
||||
applicable_stages = template_data.get("applicable_stages", [])
|
||||
|
||||
# For San Pablo, use original IDs. For La Espiga, generate new UUIDs
|
||||
if tenant_id == DEMO_TENANT_SAN_PABLO:
|
||||
template_id = uuid.UUID(template_data["id"])
|
||||
else:
|
||||
# Generate deterministic UUID for La Espiga based on original ID
|
||||
base_uuid = uuid.UUID(template_data["id"])
|
||||
# Add a fixed offset to create a unique but deterministic ID
|
||||
template_id = uuid.UUID(int=base_uuid.int + 0x10000000000000000000000000000000)
|
||||
|
||||
# Create quality check template
|
||||
template = QualityCheckTemplate(
|
||||
id=template_id,
|
||||
tenant_id=tenant_id,
|
||||
name=template_data["name"],
|
||||
template_code=template_data["template_code"],
|
||||
check_type=check_type,
|
||||
category=template_data.get("category"),
|
||||
description=template_data.get("description"),
|
||||
instructions=template_data.get("instructions"),
|
||||
parameters=template_data.get("parameters"),
|
||||
thresholds=template_data.get("thresholds"),
|
||||
scoring_criteria=template_data.get("scoring_criteria"),
|
||||
is_active=template_data.get("is_active", True),
|
||||
is_required=template_data.get("is_required", False),
|
||||
is_critical=template_data.get("is_critical", False),
|
||||
weight=template_data.get("weight", 1.0),
|
||||
min_value=template_data.get("min_value"),
|
||||
max_value=template_data.get("max_value"),
|
||||
target_value=template_data.get("target_value"),
|
||||
unit=template_data.get("unit"),
|
||||
tolerance_percentage=template_data.get("tolerance_percentage"),
|
||||
applicable_stages=applicable_stages,
|
||||
created_by=SYSTEM_USER_ID,
|
||||
created_at=BASE_REFERENCE_DATE,
|
||||
updated_at=BASE_REFERENCE_DATE
|
||||
)
|
||||
|
||||
db.add(template)
|
||||
count += 1
|
||||
logger.debug(f"Created quality template: {template.name}", template_id=str(template.id))
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Successfully created {count} quality templates for {tenant_name}")
|
||||
|
||||
return {
|
||||
"tenant_id": str(tenant_id),
|
||||
"templates_created": count,
|
||||
"skipped": False
|
||||
}
|
||||
|
||||
|
||||
async def seed_all(db: AsyncSession):
|
||||
"""Seed all demo tenants with quality templates"""
|
||||
logger.info("Starting demo quality templates seed process")
|
||||
|
||||
# Load quality templates data
|
||||
data = load_quality_templates_data()
|
||||
|
||||
results = []
|
||||
|
||||
# Both tenants get the same quality templates
|
||||
result_san_pablo = await seed_quality_templates_for_tenant(
|
||||
db,
|
||||
DEMO_TENANT_SAN_PABLO,
|
||||
"San Pablo - Individual Bakery",
|
||||
data["plantillas_calidad"]
|
||||
)
|
||||
results.append(result_san_pablo)
|
||||
|
||||
result_la_espiga = await seed_quality_templates_for_tenant(
|
||||
db,
|
||||
DEMO_TENANT_LA_ESPIGA,
|
||||
"La Espiga - Central Bakery",
|
||||
data["plantillas_calidad"]
|
||||
)
|
||||
results.append(result_la_espiga)
|
||||
|
||||
total_created = sum(r["templates_created"] for r in results)
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"total_templates_created": total_created,
|
||||
"status": "completed"
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main execution function"""
|
||||
# Get database URL from environment
|
||||
database_url = os.getenv("PRODUCTION_DATABASE_URL")
|
||||
if not database_url:
|
||||
logger.error("PRODUCTION_DATABASE_URL environment variable must be set")
|
||||
return 1
|
||||
|
||||
# Ensure asyncpg driver
|
||||
if database_url.startswith("postgresql://"):
|
||||
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
|
||||
# Create async engine
|
||||
engine = create_async_engine(database_url, echo=False)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
try:
|
||||
async with async_session() as session:
|
||||
result = await seed_all(session)
|
||||
|
||||
logger.info(
|
||||
"Quality templates seed completed successfully!",
|
||||
total_templates=result["total_templates_created"],
|
||||
status=result["status"]
|
||||
)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "="*60)
|
||||
print("DEMO QUALITY TEMPLATES SEED SUMMARY")
|
||||
print("="*60)
|
||||
for tenant_result in result["results"]:
|
||||
tenant_id = tenant_result["tenant_id"]
|
||||
count = tenant_result["templates_created"]
|
||||
skipped = tenant_result.get("skipped", False)
|
||||
status = "SKIPPED (already exists)" if skipped else f"CREATED {count} templates"
|
||||
print(f"Tenant {tenant_id}: {status}")
|
||||
print(f"\nTotal Templates Created: {result['total_templates_created']}")
|
||||
print("="*60 + "\n")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Quality templates seed failed: {str(e)}", exc_info=True)
|
||||
return 1
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
Reference in New Issue
Block a user