Improve demo seed

This commit is contained in:
Urtzi Alfaro
2025-10-17 07:31:14 +02:00
parent b6cb800758
commit d4060962e4
56 changed files with 8235 additions and 339 deletions

View 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
}
]
}

View 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"]
}
]
}

View 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"
]
}
}

View 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)

View 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)

View 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)