297 lines
14 KiB
Python
297 lines
14 KiB
Python
#!/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()
|