339 lines
13 KiB
Python
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)
|