#!/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)