Files
bakery-ia/services/suppliers/scripts/demo/seed_demo_suppliers.py
2025-11-30 09:12:40 +01:00

447 lines
16 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Demo Suppliers Seeding Script for Suppliers Service
Creates realistic Spanish suppliers for demo template tenants using pre-defined UUIDs
This script runs as a Kubernetes init job inside the suppliers-service container.
It populates the template tenants with a comprehensive catalog of suppliers.
Usage:
python /app/scripts/demo/seed_demo_suppliers.py
Environment Variables Required:
SUPPLIERS_DATABASE_URL - PostgreSQL connection string for suppliers database
DEMO_MODE - Set to 'production' for production seeding
LOG_LEVEL - Logging level (default: INFO)
Note: No database lookups needed - all IDs are pre-defined in the JSON file
"""
import asyncio
import uuid
import sys
import os
import json
from datetime import datetime, timezone, timedelta
from pathlib import Path
import random
from decimal import Decimal
# Add app to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import select, text
import structlog
from app.models.suppliers import (
Supplier, SupplierPriceList,
SupplierType, SupplierStatus, PaymentTerms
)
# Configure logging
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.dev.ConsoleRenderer()
]
)
logger = structlog.get_logger()
# Fixed Demo Tenant IDs (must match tenant service)
DEMO_TENANT_PROFESSIONAL = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6")
DEMO_TENANT_ENTERPRISE_CHAIN = uuid.UUID("c3d4e5f6-a7b8-49c0-d1e2-f3a4b5c6d7e8") # Enterprise parent (Obrador)
# Hardcoded SKU to Ingredient ID mapping (no database lookups needed!)
INGREDIENT_ID_MAP = {
"HAR-T55-001": "10000000-0000-0000-0000-000000000001",
"HAR-T65-002": "10000000-0000-0000-0000-000000000002",
"HAR-FUE-003": "10000000-0000-0000-0000-000000000003",
"HAR-INT-004": "10000000-0000-0000-0000-000000000004",
"HAR-CEN-005": "10000000-0000-0000-0000-000000000005",
"HAR-ESP-006": "10000000-0000-0000-0000-000000000006",
"LAC-MAN-001": "10000000-0000-0000-0000-000000000011",
"LAC-LEC-002": "10000000-0000-0000-0000-000000000012",
"LAC-NAT-003": "10000000-0000-0000-0000-000000000013",
"LAC-HUE-004": "10000000-0000-0000-0000-000000000014",
"LEV-FRE-001": "10000000-0000-0000-0000-000000000021",
"LEV-SEC-002": "10000000-0000-0000-0000-000000000022",
"BAS-SAL-001": "10000000-0000-0000-0000-000000000031",
"BAS-AZU-002": "10000000-0000-0000-0000-000000000032",
"ESP-CHO-001": "10000000-0000-0000-0000-000000000041",
"ESP-ALM-002": "10000000-0000-0000-0000-000000000042",
"ESP-VAI-004": "10000000-0000-0000-0000-000000000044",
"ESP-CRE-005": "10000000-0000-0000-0000-000000000045",
}
# Ingredient costs (for price list generation)
INGREDIENT_COSTS = {
"HAR-T55-001": 0.85,
"HAR-T65-002": 0.95,
"HAR-FUE-003": 1.15,
"HAR-INT-004": 1.20,
"HAR-CEN-005": 1.30,
"HAR-ESP-006": 2.45,
"LAC-MAN-001": 6.50,
"LAC-LEC-002": 0.95,
"LAC-NAT-003": 3.20,
"LAC-HUE-004": 0.25,
"LEV-FRE-001": 4.80,
"LEV-SEC-002": 12.50,
"BAS-SAL-001": 0.60,
"BAS-AZU-002": 0.90,
"ESP-CHO-001": 15.50,
"ESP-ALM-002": 8.90,
"ESP-VAI-004": 3.50,
"ESP-CRE-005": 7.20,
}
def load_suppliers_data():
"""Load suppliers data from JSON file"""
# Look for data file in the same directory as this script
data_file = Path(__file__).parent / "proveedores_es.json"
if not data_file.exists():
raise FileNotFoundError(
f"Suppliers data file not found: {data_file}. "
"Make sure proveedores_es.json is in the same directory as this script."
)
logger.info("Loading suppliers data", file=str(data_file))
with open(data_file, 'r', encoding='utf-8') as f:
data = json.load(f)
suppliers = data.get("proveedores", [])
logger.info(f"Loaded {len(suppliers)} suppliers from JSON")
return suppliers
async def seed_suppliers_for_tenant(
db: AsyncSession,
tenant_id: uuid.UUID,
tenant_name: str,
suppliers_data: list
) -> dict:
"""
Seed suppliers for a specific tenant using pre-defined UUIDs
Args:
db: Database session
tenant_id: UUID of the tenant
tenant_name: Name of the tenant (for logging)
suppliers_data: List of supplier dictionaries with pre-defined IDs
Returns:
Dict with seeding statistics
"""
logger.info("" * 80)
logger.info(f"Seeding suppliers for: {tenant_name}")
logger.info(f"Tenant ID: {tenant_id}")
logger.info("" * 80)
created_suppliers = 0
skipped_suppliers = 0
created_price_lists = 0
for supplier_data in suppliers_data:
supplier_name = supplier_data["name"]
# Generate tenant-specific UUID by combining base UUID with tenant ID
base_supplier_id = uuid.UUID(supplier_data["id"])
tenant_int = int(tenant_id.hex, 16)
supplier_id = uuid.UUID(int=tenant_int ^ int(base_supplier_id.hex, 16))
# Check if supplier already exists (using tenant-specific ID)
result = await db.execute(
select(Supplier).where(
Supplier.tenant_id == tenant_id,
Supplier.id == supplier_id
)
)
existing_supplier = result.scalars().first()
if existing_supplier:
logger.debug(f" ⏭️ Supplier exists, ensuring price lists: {supplier_name}")
skipped_suppliers += 1
# Don't skip - continue to create/update price lists below
else:
# Parse enums
try:
supplier_type = SupplierType(supplier_data.get("supplier_type", "ingredients"))
except ValueError:
supplier_type = SupplierType.INGREDIENTS
try:
status = SupplierStatus(supplier_data.get("status", "active"))
except ValueError:
status = SupplierStatus.ACTIVE
try:
payment_terms = PaymentTerms(supplier_data.get("payment_terms", "net_30"))
except ValueError:
payment_terms = PaymentTerms.NET_30
# Create supplier with pre-defined ID
supplier = Supplier(
id=supplier_id,
tenant_id=tenant_id,
name=supplier_name,
supplier_code=f"SUP-{created_suppliers + 1:03d}",
supplier_type=supplier_type,
status=status,
tax_id=supplier_data.get("tax_id"),
contact_person=supplier_data.get("contact_person"),
email=supplier_data.get("email"),
phone=supplier_data.get("phone"),
mobile=supplier_data.get("mobile"),
website=supplier_data.get("website"),
address_line1=supplier_data.get("address_line1"),
address_line2=supplier_data.get("address_line2"),
city=supplier_data.get("city"),
state_province=supplier_data.get("state_province"),
postal_code=supplier_data.get("postal_code"),
country=supplier_data.get("country", "España"),
payment_terms=payment_terms,
credit_limit=Decimal(str(supplier_data.get("credit_limit", 0.0))),
standard_lead_time=supplier_data.get("standard_lead_time", 3),
quality_rating=supplier_data.get("quality_rating", 4.5),
delivery_rating=supplier_data.get("delivery_rating", 4.5),
notes=supplier_data.get("notes"),
certifications=supplier_data.get("certifications", []),
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
created_by=uuid.UUID("00000000-0000-0000-0000-000000000000"), # System user
updated_by=uuid.UUID("00000000-0000-0000-0000-000000000000") # System user
)
db.add(supplier)
created_suppliers += 1
logger.debug(f" ✅ Created supplier: {supplier_name}")
# Create price lists for products using pre-defined ingredient IDs
products = supplier_data.get("products", [])
for product_sku in products:
# Get ingredient ID from hardcoded mapping (no DB lookup!)
ingredient_id_str = INGREDIENT_ID_MAP.get(product_sku)
if not ingredient_id_str:
logger.warning(f" ⚠️ Product SKU not in mapping: {product_sku}")
continue
# Generate tenant-specific ingredient ID (same as inventory seed)
base_ingredient_id = uuid.UUID(ingredient_id_str)
tenant_int = int(tenant_id.hex, 16)
ingredient_id = uuid.UUID(int=tenant_int ^ int(base_ingredient_id.hex, 16))
# Check if price list already exists
existing_price_list_result = await db.execute(
select(SupplierPriceList).where(
SupplierPriceList.tenant_id == tenant_id,
SupplierPriceList.supplier_id == supplier_id,
SupplierPriceList.inventory_product_id == ingredient_id
)
)
existing_price_list = existing_price_list_result.scalars().first()
if existing_price_list:
# Price list already exists, skip
continue
# Get base cost from hardcoded costs
base_cost = INGREDIENT_COSTS.get(product_sku, 1.0)
# Calculate supplier price (slightly vary from base cost)
price_variation = random.uniform(0.90, 1.10)
unit_price = Decimal(str(base_cost * price_variation))
# price_per_unit is same as unit_price for base quantity
price_per_unit = unit_price
price_list = SupplierPriceList(
id=uuid.uuid4(),
tenant_id=tenant_id,
supplier_id=supplier_id,
inventory_product_id=ingredient_id,
product_code=product_sku,
unit_price=unit_price,
price_per_unit=price_per_unit,
minimum_order_quantity=random.choice([1, 5, 10]),
unit_of_measure="kg",
effective_date=datetime.now(timezone.utc) - timedelta(days=90),
is_active=True,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
created_by=uuid.UUID("00000000-0000-0000-0000-000000000000"), # System user
updated_by=uuid.UUID("00000000-0000-0000-0000-000000000000") # System user
)
db.add(price_list)
created_price_lists += 1
# Commit all changes for this tenant
await db.commit()
logger.info(f" 📊 Suppliers: {created_suppliers}, Price Lists: {created_price_lists}")
logger.info("")
return {
"tenant_id": str(tenant_id),
"tenant_name": tenant_name,
"suppliers_created": created_suppliers,
"suppliers_skipped": skipped_suppliers,
"price_lists_created": created_price_lists,
"total_suppliers": len(suppliers_data)
}
async def seed_suppliers(db: AsyncSession):
"""
Seed suppliers for all demo template tenants using pre-defined IDs
Args:
db: Database session
Returns:
Dict with overall seeding statistics
"""
logger.info("=" * 80)
logger.info("🚚 Starting Demo Suppliers Seeding")
logger.info("=" * 80)
# Load suppliers data once
try:
suppliers_data = load_suppliers_data()
except FileNotFoundError as e:
logger.error(str(e))
raise
results = []
# Seed for Professional Bakery (single location)
logger.info("")
result_professional = await seed_suppliers_for_tenant(
db,
DEMO_TENANT_PROFESSIONAL,
"Panadería Artesana Madrid (Professional)",
suppliers_data
)
results.append(result_professional)
# Seed for Enterprise Parent (central production - Obrador)
logger.info("")
result_enterprise_parent = await seed_suppliers_for_tenant(
db,
DEMO_TENANT_ENTERPRISE_CHAIN,
"Panadería Central - Obrador Madrid (Enterprise Parent)",
suppliers_data
)
results.append(result_enterprise_parent)
# Calculate totals
total_suppliers = sum(r["suppliers_created"] for r in results)
total_price_lists = sum(r["price_lists_created"] for r in results)
total_skipped = sum(r["suppliers_skipped"] for r in results)
logger.info("=" * 80)
logger.info("✅ Demo Suppliers Seeding Completed")
logger.info("=" * 80)
return {
"service": "suppliers",
"tenants_seeded": len(results),
"total_suppliers_created": total_suppliers,
"total_price_lists_created": total_price_lists,
"total_skipped": total_skipped,
"results": results
}
async def main():
"""Main execution function"""
logger.info("Demo Suppliers Seeding Script Starting")
logger.info("Mode: %s", os.getenv("DEMO_MODE", "development"))
logger.info("Log Level: %s", os.getenv("LOG_LEVEL", "INFO"))
# Get database URL from environment
database_url = os.getenv("SUPPLIERS_DATABASE_URL") or os.getenv("DATABASE_URL")
if not database_url:
logger.error("❌ SUPPLIERS_DATABASE_URL or DATABASE_URL environment variable must be set")
return 1
# Convert to async URL if needed
if database_url.startswith("postgresql://"):
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
logger.info("Connecting to suppliers database")
# Create engine and session
engine = create_async_engine(
database_url,
echo=False,
pool_pre_ping=True,
pool_size=5,
max_overflow=10
)
session_maker = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False
)
try:
async with session_maker() as session:
result = await seed_suppliers(session)
logger.info("")
logger.info("📊 Seeding Summary:")
logger.info(f" ✅ Tenants seeded: {result['tenants_seeded']}")
logger.info(f" ✅ Suppliers created: {result['total_suppliers_created']}")
logger.info(f" ✅ Price lists created: {result['total_price_lists_created']}")
logger.info(f" ⏭️ Skipped: {result['total_skipped']}")
logger.info("")
# Print per-tenant details
for tenant_result in result['results']:
logger.info(
f" {tenant_result['tenant_name']}: "
f"{tenant_result['suppliers_created']} suppliers, "
f"{tenant_result['price_lists_created']} price lists"
)
logger.info("")
logger.info("🎉 Success! Supplier catalog is ready for cloning.")
logger.info("")
logger.info("Suppliers created:")
logger.info(" • Molinos San José S.L. (harinas)")
logger.info(" • Lácteos del Valle S.A. (lácteos)")
logger.info(" • Lesaffre Ibérica (levaduras)")
logger.info(" • And 9 more suppliers...")
logger.info("")
logger.info("Note: All IDs are pre-defined and hardcoded for cross-service consistency")
logger.info("")
return 0
except Exception as e:
logger.error("=" * 80)
logger.error("❌ Demo Suppliers Seeding Failed")
logger.error("=" * 80)
logger.error("Error: %s", str(e))
logger.error("", exc_info=True)
return 1
finally:
await engine.dispose()
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)