#!/usr/bin/env python3 """ Generate AI Insights Data for Professional Demo Adds realistic stock movements and production worker data to enable AI insight generation """ import json import random from datetime import datetime, timedelta from uuid import uuid4, UUID from typing import List, Dict, Any # Set random seed for reproducibility random.seed(42) # Key ingredients that need demand history (matching actual IDs in 03-inventory.json) KEY_INGREDIENTS = [ {"id": "10000000-0000-0000-0000-000000000001", "name": "Harina de Trigo T55", "avg_daily": 45.0, "variability": 0.3, "unit_cost": 0.85}, {"id": "10000000-0000-0000-0000-000000000002", "name": "Harina de Trigo T65", "avg_daily": 35.0, "variability": 0.25, "unit_cost": 0.95}, {"id": "10000000-0000-0000-0000-000000000003", "name": "Harina de Fuerza W300", "avg_daily": 25.0, "variability": 0.35, "unit_cost": 1.15}, {"id": "10000000-0000-0000-0000-000000000011", "name": "Mantequilla sin Sal", "avg_daily": 8.5, "variability": 0.35, "unit_cost": 6.50}, {"id": "10000000-0000-0000-0000-000000000012", "name": "Leche Entera Fresca", "avg_daily": 18.0, "variability": 0.3, "unit_cost": 0.95}, {"id": "10000000-0000-0000-0000-000000000014", "name": "Huevos Frescos", "avg_daily": 5.5, "variability": 0.4, "unit_cost": 3.80}, {"id": "10000000-0000-0000-0000-000000000021", "name": "Levadura Fresca", "avg_daily": 3.5, "variability": 0.4, "unit_cost": 4.20}, {"id": "10000000-0000-0000-0000-000000000031", "name": "Sal Marina Fina", "avg_daily": 2.8, "variability": 0.2, "unit_cost": 1.50}, {"id": "10000000-0000-0000-0000-000000000032", "name": "Azúcar Blanco", "avg_daily": 12.0, "variability": 0.3, "unit_cost": 1.10}, {"id": "10000000-0000-0000-0000-000000000013", "name": "Nata para Montar", "avg_daily": 4.2, "variability": 0.35, "unit_cost": 2.80}, ] # Workers with different skill levels (matching users in 02-auth.json) WORKERS = [ {"id": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6", "name": "María García (Owner - Master Baker)", "skill_level": 0.98, "shift": "morning"}, # Expert {"id": "50000000-0000-0000-0000-000000000001", "name": "Juan Panadero (Baker)", "skill_level": 0.95, "shift": "morning"}, # Very skilled {"id": "50000000-0000-0000-0000-000000000006", "name": "Isabel Producción (Production Manager)", "skill_level": 0.90, "shift": "afternoon"}, # Experienced {"id": "50000000-0000-0000-0000-000000000005", "name": "Carlos Almacén (Warehouse - Occasional Baker)", "skill_level": 0.78, "shift": "afternoon"}, # Learning ] TENANT_ID = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6" def generate_stock_movements(days: int = 90) -> List[Dict[str, Any]]: """Generate realistic stock movements for AI insights""" movements = [] # Generate PRODUCTION_USE movements (daily consumption) for day in range(days, 0, -1): for ingredient in KEY_INGREDIENTS: # Skip some days randomly (not every ingredient used every day) if random.random() < 0.15: # 15% chance to skip continue # Calculate quantity with variability base_qty = ingredient["avg_daily"] variability = ingredient["variability"] quantity = base_qty * random.uniform(1 - variability, 1 + variability) # Reduce usage on weekends (lower production) date_offset = f"BASE_TS - {day}d" day_of_week = (90 - day) % 7 # Approximate day of week if day_of_week in [5, 6]: # Weekend quantity *= 0.6 # Round to 2 decimals quantity = round(quantity, 2) movement = { "id": str(uuid4()), "tenant_id": TENANT_ID, "ingredient_id": ingredient["id"], "stock_id": None, "movement_type": "PRODUCTION_USE", "quantity": quantity, "unit_cost": ingredient["unit_cost"], "total_cost": round(quantity * ingredient["unit_cost"], 2), "quantity_before": None, "quantity_after": None, "movement_date": date_offset, "reason_code": "production_consumption", "notes": f"Daily production usage - {ingredient['name']}", "created_at": date_offset, "created_by": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6" } movements.append(movement) # Generate PURCHASE movements (supplier deliveries - weekly/bi-weekly) for ingredient in KEY_INGREDIENTS: # Calculate delivery frequency based on usage weekly_usage = ingredient["avg_daily"] * 7 delivery_qty = weekly_usage * 2 # 2 weeks of stock # Bi-weekly deliveries over 90 days = ~6-7 deliveries num_deliveries = 6 delivery_interval = days // num_deliveries for delivery_num in range(num_deliveries): day_offset = days - (delivery_num * delivery_interval) - random.randint(0, 3) if day_offset < 1: continue # Add some variability to delivery quantity qty = delivery_qty * random.uniform(0.9, 1.1) qty = round(qty, 2) movement = { "id": str(uuid4()), "tenant_id": TENANT_ID, "ingredient_id": ingredient["id"], "stock_id": None, "movement_type": "PURCHASE", "quantity": qty, "unit_cost": ingredient["unit_cost"], "total_cost": round(qty * ingredient["unit_cost"], 2), "quantity_before": None, "quantity_after": None, "movement_date": f"BASE_TS - {day_offset}d", "reason_code": "supplier_delivery", "notes": f"Weekly delivery from supplier - {ingredient['name']}", "created_at": f"BASE_TS - {day_offset}d", "created_by": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6" } movements.append(movement) # Add occasional stockout events (0 inventory remaining) # Add 5-8 stockout PRODUCTION_USE movements for _ in range(random.randint(5, 8)): ingredient = random.choice(KEY_INGREDIENTS) day_offset = random.randint(1, days) movement = { "id": str(uuid4()), "tenant_id": TENANT_ID, "ingredient_id": ingredient["id"], "stock_id": None, "movement_type": "PRODUCTION_USE", "quantity": round(ingredient["avg_daily"] * 1.3, 2), # Higher than usual "unit_cost": ingredient["unit_cost"], "total_cost": round(ingredient["avg_daily"] * 1.3 * ingredient["unit_cost"], 2), "quantity_before": round(ingredient["avg_daily"] * 0.8, 2), "quantity_after": 0.0, # Stockout! "movement_date": f"BASE_TS - {day_offset}d", "reason_code": "production_consumption_stockout", "notes": f"STOCKOUT - Ran out of {ingredient['name']} during production", "created_at": f"BASE_TS - {day_offset}d", "created_by": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6" } movements.append(movement) return movements def add_worker_data_to_batches(batches: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """Add staff_assigned and completed_at to production batches""" updated_batches = [] for batch in batches: # Skip if no yield data (can't assign skill-based worker) if batch.get("yield_percentage") is None: updated_batches.append(batch) continue # Assign worker based on yield (better yields = better workers) yield_pct = batch["yield_percentage"] if yield_pct >= 95: # Expert workers for high yields worker = random.choice(WORKERS[:2]) elif yield_pct >= 90: # Experienced workers worker = random.choice(WORKERS[1:3]) elif yield_pct >= 85: # Competent workers worker = random.choice(WORKERS[2:4]) else: # Junior workers for lower yields worker = random.choice(WORKERS[3:]) # Add staff_assigned if "staff_assigned" not in batch or not isinstance(batch["staff_assigned"], list): batch["staff_assigned"] = [] batch["staff_assigned"].append(worker["id"]) # Calculate completed_at from actual_start_time + planned_duration_minutes if batch.get("actual_start_time") and batch.get("planned_duration_minutes"): # Parse the BASE_TS offset start_time_str = batch["actual_start_time"] duration_mins = batch["planned_duration_minutes"] # Add duration to start time with some variability actual_duration = duration_mins * random.uniform(0.95, 1.15) # +/- 15% variability # Parse the start time offset to calculate completion time # Format: "BASE_TS - 6d 7h 30m" or "BASE_TS - 6d 7h" if "BASE_TS" in start_time_str: # Extract the offset parts # Convert duration to hours for easier calculation duration_hours = actual_duration / 60.0 # Parse existing offset parts = start_time_str.replace("BASE_TS", "").strip() # Simple approach: just add the duration to the hours component # Example: "- 6d 7h 30m" -> add 3.5h -> "- 6d 10h 30m" (approximately) # For simplicity, create a new timestamp offset # Don't try to parse complex string, just create a note field batch["actual_duration_minutes"] = round(actual_duration, 1) # Don't set completed_at - let the system calculate it if needed updated_batches.append(batch) return updated_batches def main(): """Generate and update JSON files with AI insights data""" print("🔧 Generating AI Insights Data for Professional Demo...") print() # 1. Generate stock movements print("📊 Generating stock movements...") stock_movements = generate_stock_movements(days=90) usage_count = len([m for m in stock_movements if m["movement_type"] == "PRODUCTION_USE"]) in_count = len([m for m in stock_movements if m["movement_type"] == "PURCHASE"]) stockout_count = len([m for m in stock_movements if m.get("quantity_after") == 0.0]) print(f" ✓ Generated {len(stock_movements)} stock movements") print(f" - PRODUCTION_USE movements: {usage_count}") print(f" - PURCHASE movements (deliveries): {in_count}") print(f" - Stockout events: {stockout_count}") print() # 2. Load and update inventory JSON print("📦 Updating 03-inventory.json...") with open("/Users/urtzialfaro/Documents/bakery-ia/shared/demo/fixtures/professional/03-inventory.json", "r") as f: inventory_data = json.load(f) # Append new movements to existing ones existing_movements = inventory_data.get("stock_movements", []) print(f" - Existing movements: {len(existing_movements)}") inventory_data["stock_movements"] = existing_movements + stock_movements print(f" - Total movements: {len(inventory_data['stock_movements'])}") # Save updated inventory with open("/Users/urtzialfaro/Documents/bakery-ia/shared/demo/fixtures/professional/03-inventory.json", "w") as f: json.dump(inventory_data, f, indent=2, ensure_ascii=False) print(" ✓ Updated inventory file") print() # 3. Load and update production JSON print("🏭 Updating 06-production.json...") with open("/Users/urtzialfaro/Documents/bakery-ia/shared/demo/fixtures/professional/06-production.json", "r") as f: production_data = json.load(f) # Update production batches with worker data original_batches = production_data.get("batches", []) print(f" - Total batches: {len(original_batches)}") updated_batches = add_worker_data_to_batches(original_batches) batches_with_workers = len([b for b in updated_batches if b.get("staff_assigned") and len(b.get("staff_assigned", [])) > 0]) batches_with_completion = len([b for b in updated_batches if b.get("completed_at")]) production_data["batches"] = updated_batches print(f" - Batches with worker_id: {batches_with_workers}") print(f" - Batches with completed_at: {batches_with_completion}") # Save updated production with open("/Users/urtzialfaro/Documents/bakery-ia/shared/demo/fixtures/professional/06-production.json", "w") as f: json.dump(production_data, f, indent=2, ensure_ascii=False) print(" ✓ Updated production file") print() # 4. Summary print("=" * 60) print("✅ AI INSIGHTS DATA GENERATION COMPLETE") print("=" * 60) print() print("📊 DATA ADDED:") print(f" • Stock movements (PRODUCTION_USE): {usage_count} records (90 days)") print(f" • Stock movements (PURCHASE): {in_count} deliveries") print(f" • Stockout events: {stockout_count}") print(f" • Worker assignments: {batches_with_workers} batches") print(f" • Completion timestamps: {batches_with_completion} batches") print() print("🎯 AI INSIGHTS READINESS:") print(" ✓ Safety Stock Optimizer: READY (90 days demand data)") print(" ✓ Yield Predictor: READY (worker data added)") print(" ✓ Sustainability Metrics: READY (existing waste data)") print() print("🚀 Next steps:") print(" 1. Test demo session creation") print(" 2. Verify AI insights generation") print(" 3. Check insight quality in frontend") print() if __name__ == "__main__": main()