Files
bakery-ia/scripts/demo/seed_demo_inventory.py
2025-10-03 14:09:34 +02:00

339 lines
13 KiB
Python

#!/usr/bin/env python3
"""
Seed Demo Inventory Data
Populates comprehensive Spanish inventory data for both demo tenants
"""
import asyncio
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy import select, delete
import structlog
import uuid
from datetime import datetime, timedelta, timezone
logger = structlog.get_logger()
# Demo tenant IDs
DEMO_TENANT_SAN_PABLO = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"
DEMO_TENANT_LA_ESPIGA = "b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7"
async def seed_inventory_for_tenant(session, tenant_id: str, business_model: str):
"""Seed inventory data for a specific tenant"""
try:
from app.models.inventory import Ingredient, Stock, StockMovement
except ImportError:
from services.inventory.app.models.inventory import Ingredient, Stock, StockMovement
logger.info(f"Seeding inventory for {business_model}", tenant_id=tenant_id)
# Check if data already exists - if so, skip seeding to avoid duplicates
result = await session.execute(select(Ingredient).where(Ingredient.tenant_id == uuid.UUID(tenant_id)).limit(1))
existing = result.scalars().first()
if existing:
logger.info(f"Demo tenant {tenant_id} already has inventory data, skipping seed")
return
if business_model == "individual_bakery":
await seed_individual_bakery_inventory(session, tenant_id)
elif business_model == "central_baker_satellite":
await seed_central_baker_inventory(session, tenant_id)
async def seed_individual_bakery_inventory(session, tenant_id: str):
"""Seed inventory for individual bakery (produces locally)"""
try:
from app.models.inventory import Ingredient, Stock
except ImportError:
from services.inventory.app.models.inventory import Ingredient, Stock
tenant_uuid = uuid.UUID(tenant_id)
# Raw ingredients for local production
ingredients_data = [
# Harinas
("Harina de Trigo 000", "INGREDIENT", "FLOUR", None, "KILOGRAMS", 25.0, 50.0, 200.0, 2.50, "Molinos del Valle"),
("Harina Integral", "INGREDIENT", "FLOUR", None, "KILOGRAMS", 15.0, 30.0, 100.0, 3.20, "Bio Natural"),
("Harina de Centeno", "INGREDIENT", "FLOUR", None, "KILOGRAMS", 10.0, 20.0, 50.0, 3.50, "Ecológica"),
# Levaduras
("Levadura Fresca", "INGREDIENT", "YEAST", None, "KILOGRAMS", 1.0, 2.5, 10.0, 8.50, "Levapan"),
("Levadura Seca Activa", "INGREDIENT", "YEAST", None, "KILOGRAMS", 0.5, 1.0, 5.0, 12.00, "Fleischmann"),
# Grasas
("Mantequilla", "INGREDIENT", "FATS", None, "KILOGRAMS", 3.0, 8.0, 25.0, 6.80, "La Serenísima"),
("Aceite de Oliva Virgen Extra", "INGREDIENT", "FATS", None, "LITERS", 2.0, 5.0, 20.0, 15.50, "Cocinero"),
# Lácteos y Huevos
("Huevos Frescos", "INGREDIENT", "EGGS", None, "UNITS", 36, 60, 180, 0.25, "Granja San José"),
("Leche Entera", "INGREDIENT", "DAIRY", None, "LITERS", 5.0, 12.0, 50.0, 1.80, "La Serenísima"),
("Nata para Montar", "INGREDIENT", "DAIRY", None, "LITERS", 2.0, 5.0, 20.0, 3.50, "Central Lechera"),
# Azúcares
("Azúcar Blanca", "INGREDIENT", "SUGAR", None, "KILOGRAMS", 8.0, 20.0, 100.0, 1.20, "Ledesma"),
("Azúcar Morena", "INGREDIENT", "SUGAR", None, "KILOGRAMS", 3.0, 8.0, 25.0, 2.80, "Orgánica"),
("Azúcar Glass", "INGREDIENT", "SUGAR", None, "KILOGRAMS", 2.0, 5.0, 20.0, 2.20, "Ledesma"),
# Sal y Especias
("Sal Fina", "INGREDIENT", "SALT", None, "KILOGRAMS", 2.0, 5.0, 20.0, 0.80, "Celusal"),
("Canela en Polvo", "INGREDIENT", "SPICES", None, "GRAMS", 50, 150, 500, 0.08, "Alicante"),
("Vainilla en Extracto", "INGREDIENT", "SPICES", None, "MILLILITERS", 100, 250, 1000, 0.15, "McCormick"),
# Chocolates y Aditivos
("Chocolate Negro 70%", "INGREDIENT", "ADDITIVES", None, "KILOGRAMS", 1.0, 3.0, 15.0, 8.50, "Valor"),
("Cacao en Polvo", "INGREDIENT", "ADDITIVES", None, "KILOGRAMS", 0.5, 2.0, 10.0, 6.50, "Nestlé"),
("Nueces Peladas", "INGREDIENT", "ADDITIVES", None, "KILOGRAMS", 0.5, 1.5, 8.0, 12.00, "Los Nogales"),
("Pasas de Uva", "INGREDIENT", "ADDITIVES", None, "KILOGRAMS", 1.0, 2.0, 10.0, 4.50, "Mendoza Premium"),
# Productos Terminados (producción local)
("Croissant Clásico", "FINISHED_PRODUCT", None, "CROISSANTS", "PIECES", 12, 30, 80, 1.20, None),
("Pan Integral", "FINISHED_PRODUCT", None, "BREAD", "PIECES", 8, 20, 50, 2.50, None),
("Napolitana de Chocolate", "FINISHED_PRODUCT", None, "PASTRIES", "PIECES", 10, 25, 60, 1.80, None),
("Pan de Masa Madre", "FINISHED_PRODUCT", None, "BREAD", "PIECES", 6, 15, 40, 3.50, None),
("Magdalena de Vainilla", "FINISHED_PRODUCT", None, "PASTRIES", "PIECES", 8, 20, 50, 1.00, None),
]
ingredient_map = {}
for name, product_type, ing_cat, prod_cat, uom, low_stock, reorder, reorder_qty, cost, brand in ingredients_data:
ing = Ingredient(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
name=name,
product_type=product_type,
ingredient_category=ing_cat,
product_category=prod_cat,
unit_of_measure=uom,
low_stock_threshold=low_stock,
reorder_point=reorder,
reorder_quantity=reorder_qty,
average_cost=cost,
brand=brand,
is_active=True,
is_perishable=(ing_cat in ["DAIRY", "EGGS"] if ing_cat else False),
shelf_life_days=7 if ing_cat in ["DAIRY", "EGGS"] else (365 if ing_cat else 2),
created_at=datetime.now(timezone.utc)
)
session.add(ing)
ingredient_map[name] = ing
await session.commit()
# Create stock lots
now = datetime.now(timezone.utc)
# Harina de Trigo - Good stock
harina_trigo = ingredient_map["Harina de Trigo 000"]
session.add(Stock(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
ingredient_id=harina_trigo.id,
production_stage="raw_ingredient",
current_quantity=120.0,
reserved_quantity=15.0,
available_quantity=105.0,
batch_number=f"HARINA-TRI-{now.strftime('%Y%m%d')}-001",
received_date=now - timedelta(days=5),
expiration_date=now + timedelta(days=360),
unit_cost=2.50,
total_cost=300.0,
storage_location="Almacén Principal - Estante A1",
is_available=True,
is_expired=False,
quality_status="good",
created_at=now
))
# Levadura Fresca - Low stock (critical)
levadura = ingredient_map["Levadura Fresca"]
session.add(Stock(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
ingredient_id=levadura.id,
production_stage="raw_ingredient",
current_quantity=0.8,
reserved_quantity=0.3,
available_quantity=0.5,
batch_number=f"LEVAD-FRE-{now.strftime('%Y%m%d')}-001",
received_date=now - timedelta(days=2),
expiration_date=now + timedelta(days=5),
unit_cost=8.50,
total_cost=6.8,
storage_location="Cámara Fría - Nivel 2",
is_available=True,
is_expired=False,
quality_status="good",
created_at=now
))
# Croissants - Fresh batch
croissant = ingredient_map["Croissant Clásico"]
session.add(Stock(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
ingredient_id=croissant.id,
production_stage="fully_baked",
current_quantity=35,
reserved_quantity=5,
available_quantity=30,
batch_number=f"CROIS-FRESH-{now.strftime('%Y%m%d')}-001",
received_date=now - timedelta(hours=4),
expiration_date=now + timedelta(hours=20),
unit_cost=1.20,
total_cost=42.0,
storage_location="Vitrina Principal - Nivel 1",
is_available=True,
is_expired=False,
quality_status="good",
created_at=now
))
await session.commit()
logger.info("Individual bakery inventory seeded")
async def seed_central_baker_inventory(session, tenant_id: str):
"""Seed inventory for central baker satellite (receives products)"""
try:
from app.models.inventory import Ingredient, Stock
except ImportError:
from services.inventory.app.models.inventory import Ingredient, Stock
tenant_uuid = uuid.UUID(tenant_id)
# Finished and par-baked products from central baker
ingredients_data = [
# Productos Pre-Horneados (del obrador central)
("Croissant Pre-Horneado", "FINISHED_PRODUCT", None, "CROISSANTS", "PIECES", 20, 50, 150, 0.85, "Obrador Central"),
("Pan Baguette Pre-Horneado", "FINISHED_PRODUCT", None, "BREAD", "PIECES", 15, 40, 120, 1.20, "Obrador Central"),
("Napolitana Pre-Horneada", "FINISHED_PRODUCT", None, "PASTRIES", "PIECES", 15, 35, 100, 1.50, "Obrador Central"),
("Pan de Molde Pre-Horneado", "FINISHED_PRODUCT", None, "BREAD", "PIECES", 10, 25, 80, 1.80, "Obrador Central"),
# Productos Terminados (listos para venta)
("Croissant de Mantequilla", "FINISHED_PRODUCT", None, "CROISSANTS", "PIECES", 15, 40, 100, 1.20, "Obrador Central"),
("Palmera de Hojaldre", "FINISHED_PRODUCT", None, "PASTRIES", "PIECES", 10, 30, 80, 2.20, "Obrador Central"),
("Magdalena Tradicional", "FINISHED_PRODUCT", None, "PASTRIES", "PIECES", 12, 30, 80, 1.00, "Obrador Central"),
("Empanada de Atún", "FINISHED_PRODUCT", None, "OTHER_PRODUCTS", "PIECES", 8, 20, 60, 3.50, "Obrador Central"),
("Pan Integral de Molde", "FINISHED_PRODUCT", None, "BREAD", "PIECES", 10, 25, 75, 2.80, "Obrador Central"),
# Algunos ingredientes básicos
("Café en Grano", "INGREDIENT", "OTHER", None, "KILOGRAMS", 2.0, 5.0, 20.0, 18.50, "Lavazza"),
("Leche para Cafetería", "INGREDIENT", "DAIRY", None, "LITERS", 10.0, 20.0, 80.0, 1.50, "Central Lechera"),
("Azúcar para Cafetería", "INGREDIENT", "SUGAR", None, "KILOGRAMS", 3.0, 8.0, 30.0, 1.00, "Azucarera"),
]
ingredient_map = {}
for name, product_type, ing_cat, prod_cat, uom, low_stock, reorder, reorder_qty, cost, brand in ingredients_data:
ing = Ingredient(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
name=name,
product_type=product_type,
ingredient_category=ing_cat,
product_category=prod_cat,
unit_of_measure=uom,
low_stock_threshold=low_stock,
reorder_point=reorder,
reorder_quantity=reorder_qty,
average_cost=cost,
brand=brand,
is_active=True,
is_perishable=True,
shelf_life_days=3,
created_at=datetime.now(timezone.utc)
)
session.add(ing)
ingredient_map[name] = ing
await session.commit()
# Create stock lots
now = datetime.now(timezone.utc)
# Croissants pre-horneados
croissant_pre = ingredient_map["Croissant Pre-Horneado"]
session.add(Stock(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
ingredient_id=croissant_pre.id,
production_stage="par_baked",
current_quantity=75,
reserved_quantity=15,
available_quantity=60,
batch_number=f"CROIS-PAR-{now.strftime('%Y%m%d')}-001",
received_date=now - timedelta(days=1),
expiration_date=now + timedelta(days=4),
unit_cost=0.85,
total_cost=63.75,
storage_location="Congelador - Sección A",
is_available=True,
is_expired=False,
quality_status="good",
created_at=now
))
# Palmeras terminadas
palmera = ingredient_map["Palmera de Hojaldre"]
session.add(Stock(
id=uuid.uuid4(),
tenant_id=tenant_uuid,
ingredient_id=palmera.id,
production_stage="fully_baked",
current_quantity=28,
reserved_quantity=4,
available_quantity=24,
batch_number=f"PALM-{now.strftime('%Y%m%d')}-001",
received_date=now - timedelta(hours=3),
expiration_date=now + timedelta(hours=45),
unit_cost=2.20,
total_cost=61.6,
storage_location="Vitrina Pasteles - Nivel 2",
is_available=True,
is_expired=False,
quality_status="good",
created_at=now
))
await session.commit()
logger.info("Central baker satellite inventory seeded")
async def seed_demo_inventory():
"""Main seeding function"""
database_url = os.getenv("INVENTORY_DATABASE_URL")
if not database_url:
logger.error("INVENTORY_DATABASE_URL not set")
return False
engine = create_async_engine(database_url, echo=False)
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
try:
async with session_factory() as session:
# Seed both demo tenants
await seed_inventory_for_tenant(session, DEMO_TENANT_SAN_PABLO, "individual_bakery")
await seed_inventory_for_tenant(session, DEMO_TENANT_LA_ESPIGA, "central_baker_satellite")
logger.info("Demo inventory data seeded successfully")
return True
except Exception as e:
logger.error(f"Failed to seed inventory: {str(e)}")
import traceback
traceback.print_exc()
return False
finally:
await engine.dispose()
if __name__ == "__main__":
result = asyncio.run(seed_demo_inventory())
sys.exit(0 if result else 1)