#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Demo Purchase Orders Seeding Script for Suppliers Service Creates realistic PO scenarios in various states for demo purposes This script creates: - 3 PENDING_APPROVAL POs (created today, need user action) - 2 APPROVED POs (approved yesterday, in progress) - 1 AUTO_APPROVED PO (small amount, trusted supplier) - 2 COMPLETED POs (delivered last week) - 1 REJECTED PO (quality concerns) - 1 CANCELLED PO (supplier unavailable) """ import asyncio import uuid import sys import os import random from datetime import datetime, timezone, timedelta, date from pathlib import Path 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 import structlog from app.models.suppliers import ( Supplier, PurchaseOrder, PurchaseOrderItem, PurchaseOrderStatus, SupplierStatus, SupplierType ) # Configure logging logger = structlog.get_logger() # Demo tenant IDs (match those from orders service) DEMO_TENANT_IDS = [ uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"), # San Pablo uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # La Espiga ] # System user ID for auto-approvals SYSTEM_USER_ID = uuid.UUID("00000000-0000-0000-0000-000000000001") async def create_or_get_supplier( db: AsyncSession, tenant_id: uuid.UUID, name: str, supplier_type: SupplierType, trust_score: float = 0.0, is_preferred: bool = False, auto_approve_enabled: bool = False ) -> Supplier: """Create or get a demo supplier""" # Check if supplier exists result = await db.execute( select(Supplier).where( Supplier.tenant_id == tenant_id, Supplier.name == name ) ) existing = result.scalar_one_or_none() if existing: return existing # Create new supplier supplier = Supplier( id=uuid.uuid4(), tenant_id=tenant_id, name=name, supplier_code=f"SUP-{name[:3].upper()}", supplier_type=supplier_type, status=SupplierStatus.active, contact_person=f"Contact {name}", email=f"contact@{name.lower().replace(' ', '')}.com", phone="+34 91 555 " + str(random.randint(1000, 9999)), city="Madrid", country="España", standard_lead_time=random.randint(1, 3), quality_rating=random.uniform(4.0, 5.0), delivery_rating=random.uniform(4.0, 5.0), total_orders=random.randint(20, 100), total_amount=Decimal(str(random.uniform(10000, 50000))), # Trust metrics trust_score=trust_score, is_preferred_supplier=is_preferred, auto_approve_enabled=auto_approve_enabled, total_pos_count=random.randint(25, 80), approved_pos_count=random.randint(24, 78), on_time_delivery_rate=random.uniform(0.85, 0.98), fulfillment_rate=random.uniform(0.90, 0.99), last_performance_update=datetime.now(timezone.utc), approved_by=SYSTEM_USER_ID, approved_at=datetime.now(timezone.utc) - timedelta(days=30), created_by=SYSTEM_USER_ID, updated_by=SYSTEM_USER_ID ) db.add(supplier) await db.flush() logger.info(f"Created supplier: {name}", supplier_id=str(supplier.id)) return supplier async def create_purchase_order( db: AsyncSession, tenant_id: uuid.UUID, supplier: Supplier, status: PurchaseOrderStatus, total_amount: Decimal, created_offset_days: int = 0, auto_approved: bool = False, priority: str = "normal", items_data: list = None ) -> PurchaseOrder: """Create a purchase order with items""" created_at = datetime.now(timezone.utc) + timedelta(days=created_offset_days) required_delivery = created_at + timedelta(days=random.randint(3, 7)) # Generate PO number po_number = f"PO-{datetime.now().year}-{random.randint(1000, 9999)}" # Calculate amounts subtotal = total_amount tax_amount = subtotal * Decimal("0.10") # 10% IVA shipping_cost = Decimal(str(random.uniform(0, 20))) total = subtotal + tax_amount + shipping_cost # Create PO po = PurchaseOrder( id=uuid.uuid4(), tenant_id=tenant_id, supplier_id=supplier.id, po_number=po_number, status=status, priority=priority, order_date=created_at, required_delivery_date=required_delivery, subtotal=subtotal, tax_amount=tax_amount, shipping_cost=shipping_cost, discount_amount=Decimal("0.00"), total_amount=total, notes=f"Auto-generated demo PO from procurement plan" if not auto_approved else f"Auto-approved: Amount €{subtotal:.2f} within threshold", created_at=created_at, updated_at=created_at, created_by=SYSTEM_USER_ID, updated_by=SYSTEM_USER_ID ) # Set approval data if approved if status in [PurchaseOrderStatus.approved, PurchaseOrderStatus.sent_to_supplier, PurchaseOrderStatus.confirmed, PurchaseOrderStatus.completed]: po.approved_at = created_at + timedelta(hours=random.randint(1, 6)) po.approved_by = SYSTEM_USER_ID if auto_approved else uuid.uuid4() if auto_approved: po.notes = f"{po.notes}\nAuto-approved by system based on trust score and amount" # Set sent/confirmed dates if status in [PurchaseOrderStatus.sent_to_supplier, PurchaseOrderStatus.confirmed, PurchaseOrderStatus.completed]: po.sent_to_supplier_at = po.approved_at + timedelta(hours=2) if status in [PurchaseOrderStatus.confirmed, PurchaseOrderStatus.completed]: po.supplier_confirmation_date = po.sent_to_supplier_at + timedelta(hours=random.randint(4, 24)) db.add(po) await db.flush() # Create items if not items_data: items_data = [ {"name": "Harina de Trigo T55", "quantity": 100, "unit_price": 0.85, "uom": "kg"}, {"name": "Levadura Fresca", "quantity": 5, "unit_price": 4.50, "uom": "kg"}, {"name": "Sal Marina", "quantity": 10, "unit_price": 1.20, "uom": "kg"} ] for idx, item_data in enumerate(items_data, 1): ordered_qty = int(item_data["quantity"]) unit_price = Decimal(str(item_data["unit_price"])) line_total = Decimal(str(ordered_qty)) * unit_price item = PurchaseOrderItem( id=uuid.uuid4(), purchase_order_id=po.id, tenant_id=tenant_id, inventory_product_id=uuid.uuid4(), # Would link to actual inventory items product_code=f"PROD-{item_data['name'][:3].upper()}", ordered_quantity=ordered_qty, received_quantity=ordered_qty if status == PurchaseOrderStatus.completed else 0, remaining_quantity=0 if status == PurchaseOrderStatus.completed else ordered_qty, unit_price=unit_price, line_total=line_total, unit_of_measure=item_data["uom"], item_notes=f"Demo item: {item_data['name']}" ) db.add(item) logger.info(f"Created PO: {po_number}", po_id=str(po.id), status=status.value, amount=float(total)) return po async def seed_purchase_orders_for_tenant(db: AsyncSession, tenant_id: uuid.UUID): """Seed purchase orders for a specific tenant""" logger.info("Seeding purchase orders", tenant_id=str(tenant_id)) # Create/get suppliers with different trust levels supplier_high_trust = await create_or_get_supplier( db, tenant_id, "Panadería Central S.L.", SupplierType.ingredients, trust_score=0.92, is_preferred=True, auto_approve_enabled=True ) supplier_medium_trust = await create_or_get_supplier( db, tenant_id, "Distribuidora Madrid", SupplierType.ingredients, trust_score=0.75, is_preferred=True, auto_approve_enabled=False ) supplier_new = await create_or_get_supplier( db, tenant_id, "Nuevos Suministros SA", SupplierType.ingredients, trust_score=0.50, is_preferred=False, auto_approve_enabled=False ) supplier_packaging = await create_or_get_supplier( db, tenant_id, "Embalajes Premium", SupplierType.packaging, trust_score=0.88, is_preferred=True, auto_approve_enabled=True ) pos_created = [] # 1. PENDING_APPROVAL - Critical/Urgent (created today) po1 = await create_purchase_order( db, tenant_id, supplier_medium_trust, PurchaseOrderStatus.pending_approval, Decimal("1234.56"), created_offset_days=0, priority="high", items_data=[ {"name": "Harina Integral Ecológica", "quantity": 150, "unit_price": 1.20, "uom": "kg"}, {"name": "Semillas de Girasol", "quantity": 20, "unit_price": 3.50, "uom": "kg"}, {"name": "Miel de Azahar", "quantity": 10, "unit_price": 8.90, "uom": "kg"} ] ) pos_created.append(po1) # 2. PENDING_APPROVAL - Medium amount, new supplier (created today) po2 = await create_purchase_order( db, tenant_id, supplier_new, PurchaseOrderStatus.pending_approval, Decimal("789.00"), created_offset_days=0, items_data=[ {"name": "Aceite de Oliva Virgen", "quantity": 30, "unit_price": 8.50, "uom": "l"}, {"name": "Azúcar Moreno", "quantity": 50, "unit_price": 1.80, "uom": "kg"} ] ) pos_created.append(po2) # 3. PENDING_APPROVAL - Large amount (created yesterday) po3 = await create_purchase_order( db, tenant_id, supplier_medium_trust, PurchaseOrderStatus.pending_approval, Decimal("2500.00"), created_offset_days=-1, priority="normal", items_data=[ {"name": "Harina de Fuerza T65", "quantity": 500, "unit_price": 0.95, "uom": "kg"}, {"name": "Mantequilla Premium", "quantity": 80, "unit_price": 5.20, "uom": "kg"}, {"name": "Huevos Categoría A", "quantity": 600, "unit_price": 0.22, "uom": "unidad"} ] ) pos_created.append(po3) # 4. APPROVED (auto-approved, small amount, trusted supplier) po4 = await create_purchase_order( db, tenant_id, supplier_high_trust, PurchaseOrderStatus.approved, Decimal("234.50"), created_offset_days=0, auto_approved=True, items_data=[ {"name": "Levadura Seca", "quantity": 5, "unit_price": 6.90, "uom": "kg"}, {"name": "Sal Fina", "quantity": 25, "unit_price": 0.85, "uom": "kg"} ] ) pos_created.append(po4) # 5. APPROVED (manually approved yesterday) po5 = await create_purchase_order( db, tenant_id, supplier_packaging, PurchaseOrderStatus.approved, Decimal("456.78"), created_offset_days=-1, items_data=[ {"name": "Bolsas de Papel Kraft", "quantity": 1000, "unit_price": 0.12, "uom": "unidad"}, {"name": "Cajas de Cartón Grande", "quantity": 200, "unit_price": 0.45, "uom": "unidad"} ] ) pos_created.append(po5) # 6. COMPLETED (delivered last week) po6 = await create_purchase_order( db, tenant_id, supplier_high_trust, PurchaseOrderStatus.completed, Decimal("1567.80"), created_offset_days=-7, items_data=[ {"name": "Harina T55 Premium", "quantity": 300, "unit_price": 0.90, "uom": "kg"}, {"name": "Chocolate Negro 70%", "quantity": 40, "unit_price": 7.80, "uom": "kg"} ] ) pos_created.append(po6) # 7. COMPLETED (delivered 5 days ago) po7 = await create_purchase_order( db, tenant_id, supplier_medium_trust, PurchaseOrderStatus.completed, Decimal("890.45"), created_offset_days=-5, items_data=[ {"name": "Nueces Peladas", "quantity": 20, "unit_price": 12.50, "uom": "kg"}, {"name": "Pasas Sultanas", "quantity": 15, "unit_price": 4.30, "uom": "kg"} ] ) pos_created.append(po7) # 8. CANCELLED (supplier unavailable) po8 = await create_purchase_order( db, tenant_id, supplier_new, PurchaseOrderStatus.cancelled, Decimal("345.00"), created_offset_days=-3, items_data=[ {"name": "Avellanas Tostadas", "quantity": 25, "unit_price": 11.80, "uom": "kg"} ] ) po8.rejection_reason = "Supplier unable to deliver - stock unavailable" po8.notes = "Cancelled: Supplier stock unavailable at required delivery date" pos_created.append(po8) # 9. DISPUTED (quality issues) po9 = await create_purchase_order( db, tenant_id, supplier_medium_trust, PurchaseOrderStatus.disputed, Decimal("678.90"), created_offset_days=-4, priority="high", items_data=[ {"name": "Cacao en Polvo", "quantity": 30, "unit_price": 18.50, "uom": "kg"}, {"name": "Vainilla en Rama", "quantity": 2, "unit_price": 45.20, "uom": "kg"} ] ) po9.rejection_reason = "Quality below specifications - requesting replacement" po9.notes = "DISPUTED: Quality issue reported - batch rejected, requesting replacement or refund" pos_created.append(po9) await db.commit() logger.info( f"Successfully created {len(pos_created)} purchase orders for tenant", tenant_id=str(tenant_id), pending_approval=3, approved=2, completed=2, cancelled=1, disputed=1 ) return pos_created async def seed_all(db: AsyncSession): """Seed all demo tenants with purchase orders""" logger.info("Starting demo purchase orders seed process") all_pos = [] for tenant_id in DEMO_TENANT_IDS: # Check if POs already exist result = await db.execute( select(PurchaseOrder).where(PurchaseOrder.tenant_id == tenant_id).limit(1) ) existing = result.scalar_one_or_none() if existing: logger.info(f"Purchase orders already exist for tenant {tenant_id}, skipping") continue pos = await seed_purchase_orders_for_tenant(db, tenant_id) all_pos.extend(pos) return { "total_pos_created": len(all_pos), "tenants_seeded": len(DEMO_TENANT_IDS), "status": "completed" } async def main(): """Main execution function""" # Get database URL from environment database_url = os.getenv("SUPPLIERS_DATABASE_URL") if not database_url: logger.error("SUPPLIERS_DATABASE_URL environment variable must be set") return 1 # Ensure asyncpg driver if database_url.startswith("postgresql://"): database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1) # Create async engine engine = create_async_engine(database_url, echo=False) async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) try: async with async_session() as session: result = await seed_all(session) logger.info( "Purchase orders seed completed successfully!", total_pos=result["total_pos_created"], tenants=result["tenants_seeded"] ) # Print summary print("\n" + "="*60) print("DEMO PURCHASE ORDERS SEED SUMMARY") print("="*60) print(f"Total POs Created: {result['total_pos_created']}") print(f"Tenants Seeded: {result['tenants_seeded']}") print("\nPO Distribution:") print(" - 3 PENDING_APPROVAL (need user action)") print(" - 2 APPROVED (in progress)") print(" - 2 COMPLETED (delivered)") print(" - 1 CANCELLED (supplier issue)") print(" - 1 DISPUTED (quality issue)") print("="*60 + "\n") return 0 except Exception as e: logger.error(f"Purchase orders seed failed: {str(e)}", exc_info=True) return 1 finally: await engine.dispose() if __name__ == "__main__": exit_code = asyncio.run(main()) sys.exit(exit_code)