Improve the frontend 3

This commit is contained in:
Urtzi Alfaro
2025-10-30 21:08:07 +01:00
parent 36217a2729
commit 63f5c6d512
184 changed files with 21512 additions and 7442 deletions

View File

@@ -11,14 +11,18 @@ import uuid
from datetime import datetime, timezone, timedelta, date
from typing import Optional
import os
import sys
from pathlib import Path
# Add shared path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent))
from app.core.database import get_db
from app.models.suppliers import (
Supplier, SupplierPriceList, PurchaseOrder, PurchaseOrderItem,
Delivery, DeliveryItem, SupplierQualityReview, SupplierInvoice,
SupplierStatus, PurchaseOrderStatus, DeliveryStatus, InvoiceStatus,
QualityRating, DeliveryRating
Supplier, SupplierPriceList, SupplierQualityReview,
SupplierStatus, QualityRating
)
from shared.utils.demo_dates import adjust_date_for_demo, BASE_REFERENCE_DATE
logger = structlog.get_logger()
router = APIRouter(prefix="/internal/demo", tags=["internal"])
@@ -45,6 +49,7 @@ async def clone_demo_data(
virtual_tenant_id: str,
demo_account_type: str,
session_id: Optional[str] = None,
session_created_at: Optional[str] = None,
db: AsyncSession = Depends(get_db),
_: bool = Depends(verify_internal_api_key)
):
@@ -54,10 +59,7 @@ async def clone_demo_data(
Clones:
- Suppliers (vendor master data)
- Supplier price lists (product pricing)
- Purchase orders with items
- Deliveries with items
- Quality reviews
- Supplier invoices
Args:
base_tenant_id: Template tenant UUID to clone from
@@ -70,12 +72,22 @@ async def clone_demo_data(
"""
start_time = datetime.now(timezone.utc)
# Parse session creation time for date adjustment
if session_created_at:
try:
session_time = datetime.fromisoformat(session_created_at.replace('Z', '+00:00'))
except (ValueError, AttributeError):
session_time = start_time
else:
session_time = start_time
logger.info(
"Starting suppliers data cloning",
base_tenant_id=base_tenant_id,
virtual_tenant_id=virtual_tenant_id,
demo_account_type=demo_account_type,
session_id=session_id
session_id=session_id,
session_created_at=session_created_at
)
try:
@@ -87,20 +99,12 @@ async def clone_demo_data(
stats = {
"suppliers": 0,
"price_lists": 0,
"purchase_orders": 0,
"purchase_order_items": 0,
"deliveries": 0,
"delivery_items": 0,
"quality_reviews": 0,
"invoices": 0
"quality_reviews": 0
}
# ID mappings
supplier_id_map = {}
price_list_map = {}
po_id_map = {}
po_item_map = {}
delivery_id_map = {}
# Clone Suppliers
result = await db.execute(
@@ -231,212 +235,6 @@ async def clone_demo_data(
# Flush to get price list IDs
await db.flush()
# Clone Purchase Orders
result = await db.execute(
select(PurchaseOrder).where(PurchaseOrder.tenant_id == base_uuid)
)
base_pos = result.scalars().all()
logger.info(
"Found purchase orders to clone",
count=len(base_pos),
base_tenant=str(base_uuid)
)
# Calculate date offset
if base_pos:
max_date = max(po.order_date for po in base_pos)
today = datetime.now(timezone.utc)
date_offset = today - max_date
else:
date_offset = timedelta(days=0)
for po in base_pos:
new_po_id = uuid.uuid4()
po_id_map[po.id] = new_po_id
new_supplier_id = supplier_id_map.get(po.supplier_id, po.supplier_id)
new_po = PurchaseOrder(
id=new_po_id,
tenant_id=virtual_uuid,
supplier_id=new_supplier_id,
po_number=f"PO-{uuid.uuid4().hex[:8].upper()}", # New PO number
reference_number=po.reference_number,
status=po.status,
priority=po.priority,
order_date=po.order_date + date_offset,
required_delivery_date=po.required_delivery_date + date_offset if po.required_delivery_date else None,
estimated_delivery_date=po.estimated_delivery_date + date_offset if po.estimated_delivery_date else None,
subtotal=po.subtotal,
tax_amount=po.tax_amount,
shipping_cost=po.shipping_cost,
discount_amount=po.discount_amount,
total_amount=po.total_amount,
currency=po.currency,
delivery_address=po.delivery_address,
delivery_instructions=po.delivery_instructions,
delivery_contact=po.delivery_contact,
delivery_phone=po.delivery_phone,
requires_approval=po.requires_approval,
approved_by=po.approved_by,
approved_at=po.approved_at + date_offset if po.approved_at else None,
rejection_reason=po.rejection_reason,
sent_to_supplier_at=po.sent_to_supplier_at + date_offset if po.sent_to_supplier_at else None,
supplier_confirmation_date=po.supplier_confirmation_date + date_offset if po.supplier_confirmation_date else None,
supplier_reference=po.supplier_reference,
notes=po.notes,
internal_notes=po.internal_notes,
terms_and_conditions=po.terms_and_conditions,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
created_by=po.created_by,
updated_by=po.updated_by
)
db.add(new_po)
stats["purchase_orders"] += 1
# Flush to get PO IDs
await db.flush()
# Clone Purchase Order Items
for old_po_id, new_po_id in po_id_map.items():
result = await db.execute(
select(PurchaseOrderItem).where(PurchaseOrderItem.purchase_order_id == old_po_id)
)
po_items = result.scalars().all()
for item in po_items:
new_item_id = uuid.uuid4()
po_item_map[item.id] = new_item_id
new_price_list_id = price_list_map.get(item.price_list_item_id, item.price_list_item_id) if item.price_list_item_id else None
# Transform inventory_product_id to match virtual tenant's ingredient IDs
if item.inventory_product_id:
base_product_int = int(item.inventory_product_id.hex, 16)
base_ingredient_int = base_tenant_int ^ base_product_int
new_inventory_product_id = uuid.UUID(int=virtual_tenant_int ^ base_ingredient_int)
else:
new_inventory_product_id = None
new_item = PurchaseOrderItem(
id=new_item_id,
tenant_id=virtual_uuid,
purchase_order_id=new_po_id,
price_list_item_id=new_price_list_id,
inventory_product_id=new_inventory_product_id, # Transformed product reference
product_code=item.product_code,
ordered_quantity=item.ordered_quantity,
unit_of_measure=item.unit_of_measure,
unit_price=item.unit_price,
line_total=item.line_total,
received_quantity=item.received_quantity,
remaining_quantity=item.remaining_quantity,
quality_requirements=item.quality_requirements,
item_notes=item.item_notes,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.add(new_item)
stats["purchase_order_items"] += 1
# Flush to get PO item IDs
await db.flush()
# Clone Deliveries
result = await db.execute(
select(Delivery).where(Delivery.tenant_id == base_uuid)
)
base_deliveries = result.scalars().all()
logger.info(
"Found deliveries to clone",
count=len(base_deliveries),
base_tenant=str(base_uuid)
)
for delivery in base_deliveries:
new_delivery_id = uuid.uuid4()
delivery_id_map[delivery.id] = new_delivery_id
new_po_id = po_id_map.get(delivery.purchase_order_id, delivery.purchase_order_id)
new_supplier_id = supplier_id_map.get(delivery.supplier_id, delivery.supplier_id)
new_delivery = Delivery(
id=new_delivery_id,
tenant_id=virtual_uuid,
purchase_order_id=new_po_id,
supplier_id=new_supplier_id,
delivery_number=f"DEL-{uuid.uuid4().hex[:8].upper()}", # New delivery number
supplier_delivery_note=delivery.supplier_delivery_note,
status=delivery.status,
scheduled_date=delivery.scheduled_date + date_offset if delivery.scheduled_date else None,
estimated_arrival=delivery.estimated_arrival + date_offset if delivery.estimated_arrival else None,
actual_arrival=delivery.actual_arrival + date_offset if delivery.actual_arrival else None,
completed_at=delivery.completed_at + date_offset if delivery.completed_at else None,
delivery_address=delivery.delivery_address,
delivery_contact=delivery.delivery_contact,
delivery_phone=delivery.delivery_phone,
carrier_name=delivery.carrier_name,
tracking_number=delivery.tracking_number,
inspection_passed=delivery.inspection_passed,
inspection_notes=delivery.inspection_notes,
quality_issues=delivery.quality_issues,
received_by=delivery.received_by,
received_at=delivery.received_at + date_offset if delivery.received_at else None,
notes=delivery.notes,
photos=delivery.photos,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
created_by=delivery.created_by
)
db.add(new_delivery)
stats["deliveries"] += 1
# Flush to get delivery IDs
await db.flush()
# Clone Delivery Items
for old_delivery_id, new_delivery_id in delivery_id_map.items():
result = await db.execute(
select(DeliveryItem).where(DeliveryItem.delivery_id == old_delivery_id)
)
delivery_items = result.scalars().all()
for item in delivery_items:
new_po_item_id = po_item_map.get(item.purchase_order_item_id, item.purchase_order_item_id)
# Transform inventory_product_id to match virtual tenant's ingredient IDs
if item.inventory_product_id:
base_product_int = int(item.inventory_product_id.hex, 16)
base_ingredient_int = base_tenant_int ^ base_product_int
new_inventory_product_id = uuid.UUID(int=virtual_tenant_int ^ base_ingredient_int)
else:
new_inventory_product_id = None
new_item = DeliveryItem(
id=uuid.uuid4(),
tenant_id=virtual_uuid,
delivery_id=new_delivery_id,
purchase_order_item_id=new_po_item_id,
inventory_product_id=new_inventory_product_id, # Transformed product reference
ordered_quantity=item.ordered_quantity,
delivered_quantity=item.delivered_quantity,
accepted_quantity=item.accepted_quantity,
rejected_quantity=item.rejected_quantity,
batch_lot_number=item.batch_lot_number,
expiry_date=item.expiry_date + date_offset if item.expiry_date else None,
quality_grade=item.quality_grade,
quality_issues=item.quality_issues,
rejection_reason=item.rejection_reason,
item_notes=item.item_notes,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc)
)
db.add(new_item)
stats["delivery_items"] += 1
# Clone Quality Reviews
result = await db.execute(
select(SupplierQualityReview).where(SupplierQualityReview.tenant_id == base_uuid)
@@ -445,16 +243,20 @@ async def clone_demo_data(
for review in base_reviews:
new_supplier_id = supplier_id_map.get(review.supplier_id, review.supplier_id)
new_po_id = po_id_map.get(review.purchase_order_id, review.purchase_order_id) if review.purchase_order_id else None
new_delivery_id = delivery_id_map.get(review.delivery_id, review.delivery_id) if review.delivery_id else None
# Adjust dates relative to session creation time
adjusted_review_date = adjust_date_for_demo(
review.review_date, session_time, BASE_REFERENCE_DATE
)
adjusted_follow_up_date = adjust_date_for_demo(
review.follow_up_date, session_time, BASE_REFERENCE_DATE
)
new_review = SupplierQualityReview(
id=uuid.uuid4(),
tenant_id=virtual_uuid,
supplier_id=new_supplier_id,
purchase_order_id=new_po_id,
delivery_id=new_delivery_id,
review_date=review.review_date + date_offset,
review_date=adjusted_review_date,
review_type=review.review_type,
quality_rating=review.quality_rating,
delivery_rating=review.delivery_rating,
@@ -467,57 +269,15 @@ async def clone_demo_data(
quality_issues=review.quality_issues,
corrective_actions=review.corrective_actions,
follow_up_required=review.follow_up_required,
follow_up_date=review.follow_up_date + date_offset if review.follow_up_date else None,
follow_up_date=adjusted_follow_up_date,
is_final=review.is_final,
approved_by=review.approved_by,
created_at=datetime.now(timezone.utc),
created_at=session_time,
reviewed_by=review.reviewed_by
)
db.add(new_review)
stats["quality_reviews"] += 1
# Clone Supplier Invoices
result = await db.execute(
select(SupplierInvoice).where(SupplierInvoice.tenant_id == base_uuid)
)
base_invoices = result.scalars().all()
for invoice in base_invoices:
new_supplier_id = supplier_id_map.get(invoice.supplier_id, invoice.supplier_id)
new_po_id = po_id_map.get(invoice.purchase_order_id, invoice.purchase_order_id) if invoice.purchase_order_id else None
new_invoice = SupplierInvoice(
id=uuid.uuid4(),
tenant_id=virtual_uuid,
supplier_id=new_supplier_id,
purchase_order_id=new_po_id,
invoice_number=f"INV-{uuid.uuid4().hex[:8].upper()}", # New invoice number
supplier_invoice_number=invoice.supplier_invoice_number,
status=invoice.status,
invoice_date=invoice.invoice_date + date_offset,
due_date=invoice.due_date + date_offset,
received_date=invoice.received_date + date_offset,
subtotal=invoice.subtotal,
tax_amount=invoice.tax_amount,
shipping_cost=invoice.shipping_cost,
discount_amount=invoice.discount_amount,
total_amount=invoice.total_amount,
currency=invoice.currency,
paid_amount=invoice.paid_amount,
payment_date=invoice.payment_date + date_offset if invoice.payment_date else None,
payment_reference=invoice.payment_reference,
approved_by=invoice.approved_by,
approved_at=invoice.approved_at + date_offset if invoice.approved_at else None,
rejection_reason=invoice.rejection_reason,
notes=invoice.notes,
invoice_document_url=invoice.invoice_document_url,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
created_by=invoice.created_by
)
db.add(new_invoice)
stats["invoices"] += 1
# Commit all changes
await db.commit()
@@ -592,15 +352,11 @@ async def delete_demo_data(
# Count records
supplier_count = await db.scalar(select(func.count(Supplier.id)).where(Supplier.tenant_id == virtual_uuid))
po_count = await db.scalar(select(func.count(PurchaseOrder.id)).where(PurchaseOrder.tenant_id == virtual_uuid))
price_list_count = await db.scalar(select(func.count(SupplierPriceList.id)).where(SupplierPriceList.tenant_id == virtual_uuid))
quality_review_count = await db.scalar(select(func.count(SupplierQualityReview.id)).where(SupplierQualityReview.tenant_id == virtual_uuid))
# Delete in order (child tables first)
await db.execute(delete(SupplierInvoice).where(SupplierInvoice.tenant_id == virtual_uuid))
await db.execute(delete(SupplierQualityReview).where(SupplierQualityReview.tenant_id == virtual_uuid))
await db.execute(delete(DeliveryItem).where(DeliveryItem.tenant_id == virtual_uuid))
await db.execute(delete(Delivery).where(Delivery.tenant_id == virtual_uuid))
await db.execute(delete(PurchaseOrderItem).where(PurchaseOrderItem.tenant_id == virtual_uuid))
await db.execute(delete(PurchaseOrder).where(PurchaseOrder.tenant_id == virtual_uuid))
await db.execute(delete(SupplierPriceList).where(SupplierPriceList.tenant_id == virtual_uuid))
await db.execute(delete(Supplier).where(Supplier.tenant_id == virtual_uuid))
await db.commit()
@@ -614,8 +370,9 @@ async def delete_demo_data(
"virtual_tenant_id": virtual_tenant_id,
"records_deleted": {
"suppliers": supplier_count,
"purchase_orders": po_count,
"total": supplier_count + po_count
"price_lists": price_list_count,
"quality_reviews": quality_review_count,
"total": supplier_count + price_list_count + quality_review_count
},
"duration_ms": duration_ms
}

View File

@@ -11,7 +11,9 @@ from app.core.database import database_manager
from shared.service_base import StandardFastAPIService
# Import API routers
from app.api import suppliers, deliveries, purchase_orders, supplier_operations, analytics, internal_demo
from app.api import suppliers, supplier_operations, analytics, internal_demo
# REMOVED: purchase_orders, deliveries - PO and delivery management moved to Procurement Service
# from app.api import purchase_orders, deliveries
class SuppliersService(StandardFastAPIService):
@@ -40,9 +42,10 @@ class SuppliersService(StandardFastAPIService):
def __init__(self):
# Define expected database tables for health checks
# NOTE: PO, delivery, and invoice tables moved to Procurement Service
suppliers_expected_tables = [
'suppliers', 'supplier_price_lists', 'purchase_orders', 'purchase_order_items',
'deliveries', 'delivery_items', 'supplier_quality_reviews', 'supplier_invoices',
'suppliers', 'supplier_price_lists',
'supplier_quality_reviews',
'supplier_performance_metrics', 'supplier_alerts', 'supplier_scorecards',
'supplier_benchmarks', 'alert_rules'
]
@@ -73,13 +76,10 @@ class SuppliersService(StandardFastAPIService):
return [
"supplier_management",
"vendor_onboarding",
"purchase_orders",
"delivery_tracking",
# REMOVED: "purchase_orders", "delivery_tracking", "invoice_tracking" - moved to Procurement Service
"quality_reviews",
"price_list_management",
"invoice_tracking",
"supplier_ratings",
"procurement_workflow",
"performance_tracking",
"performance_analytics",
"supplier_scorecards",
@@ -104,8 +104,7 @@ service.setup_standard_endpoints()
# Include API routers
# IMPORTANT: Order matters! More specific routes must come first
# to avoid path parameter matching issues
service.add_router(purchase_orders.router) # /suppliers/purchase-orders/...
service.add_router(deliveries.router) # /suppliers/deliveries/...
# REMOVED: purchase_orders.router, deliveries.router - PO and delivery management moved to Procurement Service
service.add_router(supplier_operations.router) # /suppliers/operations/...
service.add_router(analytics.router) # /suppliers/analytics/...
service.add_router(suppliers.router) # /suppliers/{supplier_id} - catch-all, must be last

View File

@@ -11,10 +11,11 @@ from shared.database.base import Base
AuditLog = create_audit_log_model(Base)
from .suppliers import (
Supplier, SupplierPriceList, PurchaseOrder, PurchaseOrderItem,
Delivery, DeliveryItem, SupplierQualityReview, SupplierInvoice,
SupplierType, SupplierStatus, PaymentTerms, PurchaseOrderStatus,
DeliveryStatus, QualityRating, DeliveryRating, InvoiceStatus
Supplier, SupplierPriceList, SupplierQualityReview,
SupplierType, SupplierStatus, PaymentTerms, QualityRating,
# Deprecated stubs for backward compatibility
PurchaseOrder, PurchaseOrderItem, Delivery, DeliveryItem, SupplierInvoice,
PurchaseOrderStatus, DeliveryStatus, DeliveryRating, InvoiceStatus
)
from .performance import (
@@ -27,35 +28,37 @@ __all__ = [
# Supplier Models
'Supplier',
'SupplierPriceList',
'PurchaseOrder',
'PurchaseOrderItem',
'Delivery',
'DeliveryItem',
'SupplierQualityReview',
'SupplierInvoice',
# Performance Models
'SupplierPerformanceMetric',
'SupplierAlert',
'SupplierScorecard',
'SupplierBenchmark',
'AlertRule',
# Supplier Enums
'SupplierType',
'SupplierStatus',
'PaymentTerms',
'PurchaseOrderStatus',
'DeliveryStatus',
'QualityRating',
'DeliveryRating',
'InvoiceStatus',
# Performance Enums
'AlertSeverity',
'AlertType',
'AlertStatus',
'PerformanceMetricType',
'PerformancePeriod',
"AuditLog"
"AuditLog",
# Deprecated stubs (backward compatibility only - DO NOT USE)
'PurchaseOrder',
'PurchaseOrderItem',
'Delivery',
'DeliveryItem',
'SupplierInvoice',
'PurchaseOrderStatus',
'DeliveryStatus',
'DeliveryRating',
'InvoiceStatus',
]

View File

@@ -1,7 +1,8 @@
# services/suppliers/app/models/suppliers.py
"""
Supplier & Procurement management models for Suppliers Service
Comprehensive supplier management, purchase orders, deliveries, and vendor relationships
Supplier management models for Suppliers Service
Comprehensive supplier management and vendor relationships
NOTE: Purchase orders, deliveries, and invoices have been moved to Procurement Service
"""
from sqlalchemy import Column, String, DateTime, Float, Integer, Text, Index, Boolean, Numeric, ForeignKey, Enum as SQLEnum
@@ -46,8 +47,23 @@ class PaymentTerms(enum.Enum):
credit_terms = "credit_terms"
class QualityRating(enum.Enum):
"""Quality rating scale for supplier reviews"""
excellent = 5
good = 4
average = 3
poor = 2
very_poor = 1
# ============================================================================
# DEPRECATED ENUMS - Kept for backward compatibility only
# These enums are defined here to prevent import errors, but the actual
# tables and functionality have moved to the Procurement Service
# ============================================================================
class PurchaseOrderStatus(enum.Enum):
"""Purchase order lifecycle status"""
"""DEPRECATED: Moved to Procurement Service"""
draft = "draft"
pending_approval = "pending_approval"
approved = "approved"
@@ -60,7 +76,7 @@ class PurchaseOrderStatus(enum.Enum):
class DeliveryStatus(enum.Enum):
"""Delivery status tracking"""
"""DEPRECATED: Moved to Procurement Service"""
scheduled = "scheduled"
in_transit = "in_transit"
out_for_delivery = "out_for_delivery"
@@ -70,17 +86,8 @@ class DeliveryStatus(enum.Enum):
returned = "returned"
class QualityRating(enum.Enum):
"""Quality rating scale"""
excellent = 5
good = 4
average = 3
poor = 2
very_poor = 1
class DeliveryRating(enum.Enum):
"""Delivery performance rating scale"""
"""DEPRECATED: Moved to Procurement Service"""
excellent = 5
good = 4
average = 3
@@ -89,7 +96,7 @@ class DeliveryRating(enum.Enum):
class InvoiceStatus(enum.Enum):
"""Invoice processing status"""
"""DEPRECATED: Moved to Procurement Service"""
pending = "pending"
approved = "approved"
paid = "paid"
@@ -175,7 +182,6 @@ class Supplier(Base):
# Relationships
price_lists = relationship("SupplierPriceList", back_populates="supplier", cascade="all, delete-orphan")
purchase_orders = relationship("PurchaseOrder", back_populates="supplier")
quality_reviews = relationship("SupplierQualityReview", back_populates="supplier", cascade="all, delete-orphan")
# Indexes
@@ -232,8 +238,7 @@ class SupplierPriceList(Base):
# Relationships
supplier = relationship("Supplier", back_populates="price_lists")
purchase_order_items = relationship("PurchaseOrderItem", back_populates="price_list_item")
# Indexes
__table_args__ = (
Index('ix_price_lists_tenant_supplier', 'tenant_id', 'supplier_id'),
@@ -243,242 +248,21 @@ class SupplierPriceList(Base):
)
class PurchaseOrder(Base):
"""Purchase orders to suppliers"""
__tablename__ = "purchase_orders"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
supplier_id = Column(UUID(as_uuid=True), ForeignKey('suppliers.id'), nullable=False, index=True)
# Order identification
po_number = Column(String(50), nullable=False, index=True) # Human-readable PO number
reference_number = Column(String(100), nullable=True) # Internal reference
# Order status and workflow
status = Column(SQLEnum(PurchaseOrderStatus), nullable=False, default=PurchaseOrderStatus.draft, index=True)
priority = Column(String(20), nullable=False, default="normal") # urgent, high, normal, low
# Order details
order_date = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
required_delivery_date = Column(DateTime(timezone=True), nullable=True)
estimated_delivery_date = Column(DateTime(timezone=True), nullable=True)
# Financial information
subtotal = Column(Numeric(12, 2), nullable=False, default=0.0)
tax_amount = Column(Numeric(12, 2), nullable=False, default=0.0)
shipping_cost = Column(Numeric(10, 2), nullable=False, default=0.0)
discount_amount = Column(Numeric(10, 2), nullable=False, default=0.0)
total_amount = Column(Numeric(12, 2), nullable=False, default=0.0)
currency = Column(String(3), nullable=False, default="EUR")
# Delivery information
delivery_address = Column(Text, nullable=True) # Override default address
delivery_instructions = Column(Text, nullable=True)
delivery_contact = Column(String(200), nullable=True)
delivery_phone = Column(String(30), nullable=True)
# Approval workflow
requires_approval = Column(Boolean, nullable=False, default=False)
approved_by = Column(UUID(as_uuid=True), nullable=True)
approved_at = Column(DateTime(timezone=True), nullable=True)
rejection_reason = Column(Text, nullable=True)
# Communication tracking
sent_to_supplier_at = Column(DateTime(timezone=True), nullable=True)
supplier_confirmation_date = Column(DateTime(timezone=True), nullable=True)
supplier_reference = Column(String(100), nullable=True) # Supplier's order reference
# Additional information
notes = Column(Text, nullable=True)
internal_notes = Column(Text, nullable=True) # Not shared with supplier
terms_and_conditions = Column(Text, nullable=True)
# Audit fields
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
created_by = Column(UUID(as_uuid=True), nullable=False)
updated_by = Column(UUID(as_uuid=True), nullable=False)
# Relationships
supplier = relationship("Supplier", back_populates="purchase_orders")
items = relationship("PurchaseOrderItem", back_populates="purchase_order", cascade="all, delete-orphan")
deliveries = relationship("Delivery", back_populates="purchase_order")
invoices = relationship("SupplierInvoice", back_populates="purchase_order")
# Indexes
__table_args__ = (
Index('ix_purchase_orders_tenant_supplier', 'tenant_id', 'supplier_id'),
Index('ix_purchase_orders_tenant_status', 'tenant_id', 'status'),
Index('ix_purchase_orders_po_number', 'po_number'),
Index('ix_purchase_orders_order_date', 'order_date'),
Index('ix_purchase_orders_delivery_date', 'required_delivery_date'),
)
class PurchaseOrderItem(Base):
"""Individual items within purchase orders"""
__tablename__ = "purchase_order_items"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
purchase_order_id = Column(UUID(as_uuid=True), ForeignKey('purchase_orders.id'), nullable=False, index=True)
price_list_item_id = Column(UUID(as_uuid=True), ForeignKey('supplier_price_lists.id'), nullable=True, index=True)
# Product identification
inventory_product_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Reference to inventory products
product_code = Column(String(100), nullable=True) # Supplier's product code
# Order quantities
ordered_quantity = Column(Integer, nullable=False)
unit_of_measure = Column(String(20), nullable=False)
unit_price = Column(Numeric(10, 4), nullable=False)
line_total = Column(Numeric(12, 2), nullable=False)
# Delivery tracking
received_quantity = Column(Integer, nullable=False, default=0)
remaining_quantity = Column(Integer, nullable=False, default=0)
# Quality and notes
quality_requirements = Column(Text, nullable=True)
item_notes = Column(Text, nullable=True)
# Audit fields
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relationships
purchase_order = relationship("PurchaseOrder", back_populates="items")
price_list_item = relationship("SupplierPriceList", back_populates="purchase_order_items")
delivery_items = relationship("DeliveryItem", back_populates="purchase_order_item")
# Indexes
__table_args__ = (
Index('ix_po_items_tenant_po', 'tenant_id', 'purchase_order_id'),
Index('ix_po_items_inventory_product', 'inventory_product_id'),
)
class Delivery(Base):
"""Delivery tracking for purchase orders"""
__tablename__ = "deliveries"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
purchase_order_id = Column(UUID(as_uuid=True), ForeignKey('purchase_orders.id'), nullable=False, index=True)
supplier_id = Column(UUID(as_uuid=True), ForeignKey('suppliers.id'), nullable=False, index=True)
# Delivery identification
delivery_number = Column(String(50), nullable=False, index=True)
supplier_delivery_note = Column(String(100), nullable=True) # Supplier's delivery reference
# Delivery status and tracking
status = Column(SQLEnum(DeliveryStatus), nullable=False, default=DeliveryStatus.scheduled, index=True)
# Scheduling and timing
scheduled_date = Column(DateTime(timezone=True), nullable=True)
estimated_arrival = Column(DateTime(timezone=True), nullable=True)
actual_arrival = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
# Delivery details
delivery_address = Column(Text, nullable=True)
delivery_contact = Column(String(200), nullable=True)
delivery_phone = Column(String(30), nullable=True)
carrier_name = Column(String(200), nullable=True)
tracking_number = Column(String(100), nullable=True)
# Quality inspection
inspection_passed = Column(Boolean, nullable=True)
inspection_notes = Column(Text, nullable=True)
quality_issues = Column(JSONB, nullable=True) # Documented quality problems
# Received by information
received_by = Column(UUID(as_uuid=True), nullable=True) # User who received the delivery
received_at = Column(DateTime(timezone=True), nullable=True)
# Additional information
notes = Column(Text, nullable=True)
photos = Column(JSONB, nullable=True) # Photo URLs for documentation
# Audit fields
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
created_by = Column(UUID(as_uuid=True), nullable=False)
# Relationships
purchase_order = relationship("PurchaseOrder", back_populates="deliveries")
supplier = relationship("Supplier")
items = relationship("DeliveryItem", back_populates="delivery", cascade="all, delete-orphan")
# Indexes
__table_args__ = (
Index('ix_deliveries_tenant_status', 'tenant_id', 'status'),
Index('ix_deliveries_scheduled_date', 'scheduled_date'),
Index('ix_deliveries_delivery_number', 'delivery_number'),
)
class DeliveryItem(Base):
"""Individual items within deliveries"""
__tablename__ = "delivery_items"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
delivery_id = Column(UUID(as_uuid=True), ForeignKey('deliveries.id'), nullable=False, index=True)
purchase_order_item_id = Column(UUID(as_uuid=True), ForeignKey('purchase_order_items.id'), nullable=False, index=True)
# Product identification
inventory_product_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Delivery quantities
ordered_quantity = Column(Integer, nullable=False)
delivered_quantity = Column(Integer, nullable=False)
accepted_quantity = Column(Integer, nullable=False)
rejected_quantity = Column(Integer, nullable=False, default=0)
# Quality information
batch_lot_number = Column(String(100), nullable=True)
expiry_date = Column(DateTime(timezone=True), nullable=True)
quality_grade = Column(String(20), nullable=True)
# Issues and notes
quality_issues = Column(Text, nullable=True)
rejection_reason = Column(Text, nullable=True)
item_notes = Column(Text, nullable=True)
# Audit fields
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relationships
delivery = relationship("Delivery", back_populates="items")
purchase_order_item = relationship("PurchaseOrderItem", back_populates="delivery_items")
# Indexes
__table_args__ = (
Index('ix_delivery_items_tenant_delivery', 'tenant_id', 'delivery_id'),
Index('ix_delivery_items_inventory_product', 'inventory_product_id'),
)
class SupplierQualityReview(Base):
"""Quality and performance reviews for suppliers"""
__tablename__ = "supplier_quality_reviews"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
supplier_id = Column(UUID(as_uuid=True), ForeignKey('suppliers.id'), nullable=False, index=True)
purchase_order_id = Column(UUID(as_uuid=True), ForeignKey('purchase_orders.id'), nullable=True, index=True)
delivery_id = Column(UUID(as_uuid=True), ForeignKey('deliveries.id'), nullable=True, index=True)
# Review details
review_date = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
review_type = Column(String(50), nullable=False) # delivery, monthly, annual, incident
review_type = Column(String(50), nullable=False) # monthly, annual, incident
# Ratings (1-5 scale)
quality_rating = Column(SQLEnum(QualityRating), nullable=False)
delivery_rating = Column(SQLEnum(DeliveryRating), nullable=False)
delivery_rating = Column(Integer, nullable=False) # 1-5 scale
communication_rating = Column(Integer, nullable=False) # 1-5
overall_rating = Column(Float, nullable=False) # Calculated average
@@ -512,61 +296,38 @@ class SupplierQualityReview(Base):
Index('ix_quality_reviews_overall_rating', 'overall_rating'),
)
# ============================================================================
# DEPRECATED MODELS - Stub definitions for backward compatibility
# These models are defined here ONLY to prevent import errors
# The actual tables exist in the Procurement Service database, NOT here
# __table__ = None prevents SQLAlchemy from creating these tables
# ============================================================================
class SupplierInvoice(Base):
"""Invoices from suppliers"""
__tablename__ = "supplier_invoices"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
supplier_id = Column(UUID(as_uuid=True), ForeignKey('suppliers.id'), nullable=False, index=True)
purchase_order_id = Column(UUID(as_uuid=True), ForeignKey('purchase_orders.id'), nullable=True, index=True)
# Invoice identification
invoice_number = Column(String(50), nullable=False, index=True)
supplier_invoice_number = Column(String(100), nullable=False)
# Invoice status and dates
status = Column(SQLEnum(InvoiceStatus), nullable=False, default=InvoiceStatus.pending, index=True)
invoice_date = Column(DateTime(timezone=True), nullable=False)
due_date = Column(DateTime(timezone=True), nullable=False)
received_date = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
# Financial information
subtotal = Column(Numeric(12, 2), nullable=False)
tax_amount = Column(Numeric(12, 2), nullable=False, default=0.0)
shipping_cost = Column(Numeric(10, 2), nullable=False, default=0.0)
discount_amount = Column(Numeric(10, 2), nullable=False, default=0.0)
total_amount = Column(Numeric(12, 2), nullable=False)
currency = Column(String(3), nullable=False, default="EUR")
# Payment tracking
paid_amount = Column(Numeric(12, 2), nullable=False, default=0.0)
payment_date = Column(DateTime(timezone=True), nullable=True)
payment_reference = Column(String(100), nullable=True)
# Invoice validation
approved_by = Column(UUID(as_uuid=True), nullable=True)
approved_at = Column(DateTime(timezone=True), nullable=True)
rejection_reason = Column(Text, nullable=True)
# Additional information
notes = Column(Text, nullable=True)
invoice_document_url = Column(String(500), nullable=True) # PDF storage location
# Audit fields
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
created_by = Column(UUID(as_uuid=True), nullable=False)
# Relationships
supplier = relationship("Supplier")
purchase_order = relationship("PurchaseOrder", back_populates="invoices")
# Indexes
__table_args__ = (
Index('ix_invoices_tenant_supplier', 'tenant_id', 'supplier_id'),
Index('ix_invoices_tenant_status', 'tenant_id', 'status'),
Index('ix_invoices_due_date', 'due_date'),
Index('ix_invoices_invoice_number', 'invoice_number'),
)
class PurchaseOrder:
"""DEPRECATED STUB: Actual implementation in Procurement Service"""
__table__ = None # Prevent table creation
pass
class PurchaseOrderItem:
"""DEPRECATED STUB: Actual implementation in Procurement Service"""
__table__ = None # Prevent table creation
pass
class Delivery:
"""DEPRECATED STUB: Actual implementation in Procurement Service"""
__table__ = None # Prevent table creation
pass
class DeliveryItem:
"""DEPRECATED STUB: Actual implementation in Procurement Service"""
__table__ = None # Prevent table creation
pass
class SupplierInvoice:
"""DEPRECATED STUB: Actual implementation in Procurement Service"""
__table__ = None # Prevent table creation
pass

View File

@@ -4,17 +4,20 @@ Pydantic schemas for supplier-related API requests and responses
"""
from pydantic import BaseModel, Field, EmailStr
from typing import List, Optional, Dict, Any
from typing import List, Optional, Dict, Any, Union
from uuid import UUID
from datetime import datetime
from decimal import Decimal
from app.models.suppliers import (
SupplierType, SupplierStatus, PaymentTerms,
PurchaseOrderStatus, DeliveryStatus,
QualityRating, DeliveryRating, InvoiceStatus
SupplierType, SupplierStatus, PaymentTerms,
QualityRating
)
# NOTE: PO, Delivery, and Invoice schemas remain for backward compatibility
# but the actual tables and functionality have moved to Procurement Service
# TODO: These schemas should be removed once all clients migrate to Procurement Service
# ============================================================================
# SUPPLIER SCHEMAS
@@ -51,7 +54,7 @@ class SupplierCreate(BaseModel):
# Additional information
notes: Optional[str] = None
certifications: Optional[Dict[str, Any]] = None
certifications: Optional[Union[Dict[str, Any], List[str]]] = None
business_hours: Optional[Dict[str, Any]] = None
specializations: Optional[Dict[str, Any]] = None
@@ -88,7 +91,7 @@ class SupplierUpdate(BaseModel):
# Additional information
notes: Optional[str] = None
certifications: Optional[Dict[str, Any]] = None
certifications: Optional[Union[Dict[str, Any], List[str]]] = None
business_hours: Optional[Dict[str, Any]] = None
specializations: Optional[Dict[str, Any]] = None
@@ -144,7 +147,7 @@ class SupplierResponse(BaseModel):
# Additional information
notes: Optional[str] = None
certifications: Optional[Dict[str, Any]] = None
certifications: Optional[Union[Dict[str, Any], List[str]]] = None
business_hours: Optional[Dict[str, Any]] = None
specializations: Optional[Dict[str, Any]] = None
@@ -303,7 +306,7 @@ class PurchaseOrderUpdate(BaseModel):
class PurchaseOrderStatusUpdate(BaseModel):
"""Schema for updating purchase order status"""
status: PurchaseOrderStatus
status: str # PurchaseOrderStatus - moved to Procurement Service
notes: Optional[str] = None
@@ -320,7 +323,7 @@ class PurchaseOrderResponse(BaseModel):
supplier_id: UUID
po_number: str
reference_number: Optional[str] = None
status: PurchaseOrderStatus
status: str # PurchaseOrderStatus
priority: str
order_date: datetime
required_delivery_date: Optional[datetime] = None
@@ -376,7 +379,7 @@ class PurchaseOrderSummary(BaseModel):
po_number: str
supplier_id: UUID
supplier_name: Optional[str] = None
status: PurchaseOrderStatus
status: str # PurchaseOrderStatus
priority: str
order_date: datetime
required_delivery_date: Optional[datetime] = None
@@ -483,7 +486,7 @@ class DeliveryUpdate(BaseModel):
class DeliveryStatusUpdate(BaseModel):
"""Schema for updating delivery status"""
status: DeliveryStatus
status: str # DeliveryStatus
notes: Optional[str] = None
update_timestamps: bool = Field(default=True)
@@ -504,7 +507,7 @@ class DeliveryResponse(BaseModel):
supplier_id: UUID
delivery_number: str
supplier_delivery_note: Optional[str] = None
status: DeliveryStatus
status: str # DeliveryStatus
# Timing
scheduled_date: Optional[datetime] = None
@@ -554,7 +557,7 @@ class DeliverySummary(BaseModel):
supplier_name: Optional[str] = None
purchase_order_id: UUID
po_number: Optional[str] = None
status: DeliveryStatus
status: str # DeliveryStatus
scheduled_date: Optional[datetime] = None
actual_arrival: Optional[datetime] = None
inspection_passed: Optional[bool] = None
@@ -580,7 +583,7 @@ class SupplierSearchParams(BaseModel):
class PurchaseOrderSearchParams(BaseModel):
"""Search parameters for purchase orders"""
supplier_id: Optional[UUID] = None
status: Optional[PurchaseOrderStatus] = None
status: Optional[str] = None # PurchaseOrderStatus
priority: Optional[str] = None
date_from: Optional[datetime] = None
date_to: Optional[datetime] = None
@@ -592,7 +595,7 @@ class PurchaseOrderSearchParams(BaseModel):
class DeliverySearchParams(BaseModel):
"""Search parameters for deliveries"""
supplier_id: Optional[UUID] = None
status: Optional[DeliveryStatus] = None
status: Optional[str] = None # DeliveryStatus
date_from: Optional[datetime] = None
date_to: Optional[datetime] = None
search_term: Optional[str] = Field(None, max_length=100)

View File

@@ -4,15 +4,14 @@ Services package for the Supplier service
"""
from .supplier_service import SupplierService
from .purchase_order_service import PurchaseOrderService
from .delivery_service import DeliveryService
# REMOVED: PurchaseOrderService, DeliveryService - moved to Procurement Service
# from .purchase_order_service import PurchaseOrderService
# from .delivery_service import DeliveryService
from .performance_service import PerformanceTrackingService, AlertService
from .dashboard_service import DashboardService
__all__ = [
'SupplierService',
'PurchaseOrderService',
'DeliveryService',
'PerformanceTrackingService',
'AlertService',
'DashboardService'

View File

@@ -1,7 +1,7 @@
"""initial_schema_20251015_1229
Revision ID: 93d6ea3dc888
Revises:
Revises:
Create Date: 2025-10-15 12:29:52.767171+02:00
"""
@@ -157,6 +157,14 @@ def upgrade() -> None:
sa.Column('delivery_rating', sa.Float(), nullable=True),
sa.Column('total_orders', sa.Integer(), nullable=False),
sa.Column('total_amount', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('trust_score', sa.Float(), nullable=False, server_default='0.0'),
sa.Column('is_preferred_supplier', sa.Boolean(), nullable=False, server_default='false'),
sa.Column('auto_approve_enabled', sa.Boolean(), nullable=False, server_default='false'),
sa.Column('total_pos_count', sa.Integer(), nullable=False, server_default='0'),
sa.Column('approved_pos_count', sa.Integer(), nullable=False, server_default='0'),
sa.Column('on_time_delivery_rate', sa.Float(), nullable=False, server_default='0.0'),
sa.Column('fulfillment_rate', sa.Float(), nullable=False, server_default='0.0'),
sa.Column('last_performance_update', sa.DateTime(timezone=True), nullable=True),
sa.Column('approved_by', sa.UUID(), nullable=True),
sa.Column('approved_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('rejection_reason', sa.Text(), nullable=True),
@@ -179,52 +187,9 @@ def upgrade() -> None:
op.create_index('ix_suppliers_tenant_name', 'suppliers', ['tenant_id', 'name'], unique=False)
op.create_index('ix_suppliers_tenant_status', 'suppliers', ['tenant_id', 'status'], unique=False)
op.create_index('ix_suppliers_tenant_type', 'suppliers', ['tenant_id', 'supplier_type'], unique=False)
op.create_table('purchase_orders',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('supplier_id', sa.UUID(), nullable=False),
sa.Column('po_number', sa.String(length=50), nullable=False),
sa.Column('reference_number', sa.String(length=100), nullable=True),
sa.Column('status', sa.Enum('draft', 'pending_approval', 'approved', 'sent_to_supplier', 'confirmed', 'partially_received', 'completed', 'cancelled', 'disputed', name='purchaseorderstatus'), nullable=False),
sa.Column('priority', sa.String(length=20), nullable=False),
sa.Column('order_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('required_delivery_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('estimated_delivery_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('subtotal', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('tax_amount', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('shipping_cost', sa.Numeric(precision=10, scale=2), nullable=False),
sa.Column('discount_amount', sa.Numeric(precision=10, scale=2), nullable=False),
sa.Column('total_amount', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('currency', sa.String(length=3), nullable=False),
sa.Column('delivery_address', sa.Text(), nullable=True),
sa.Column('delivery_instructions', sa.Text(), nullable=True),
sa.Column('delivery_contact', sa.String(length=200), nullable=True),
sa.Column('delivery_phone', sa.String(length=30), nullable=True),
sa.Column('requires_approval', sa.Boolean(), nullable=False),
sa.Column('approved_by', sa.UUID(), nullable=True),
sa.Column('approved_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('rejection_reason', sa.Text(), nullable=True),
sa.Column('sent_to_supplier_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('supplier_confirmation_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('supplier_reference', sa.String(length=100), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('internal_notes', sa.Text(), nullable=True),
sa.Column('terms_and_conditions', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_by', sa.UUID(), nullable=False),
sa.Column('updated_by', sa.UUID(), nullable=False),
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_purchase_orders_delivery_date', 'purchase_orders', ['required_delivery_date'], unique=False)
op.create_index('ix_purchase_orders_order_date', 'purchase_orders', ['order_date'], unique=False)
op.create_index('ix_purchase_orders_po_number', 'purchase_orders', ['po_number'], unique=False)
op.create_index(op.f('ix_purchase_orders_status'), 'purchase_orders', ['status'], unique=False)
op.create_index(op.f('ix_purchase_orders_supplier_id'), 'purchase_orders', ['supplier_id'], unique=False)
op.create_index(op.f('ix_purchase_orders_tenant_id'), 'purchase_orders', ['tenant_id'], unique=False)
op.create_index('ix_purchase_orders_tenant_status', 'purchase_orders', ['tenant_id', 'status'], unique=False)
op.create_index('ix_purchase_orders_tenant_supplier', 'purchase_orders', ['tenant_id', 'supplier_id'], unique=False)
op.create_index('ix_suppliers_trust_score', 'suppliers', ['trust_score'], unique=False)
op.create_index('ix_suppliers_preferred', 'suppliers', ['is_preferred_supplier'], unique=False)
op.create_index('ix_suppliers_auto_approve', 'suppliers', ['auto_approve_enabled'], unique=False)
op.create_table('supplier_performance_metrics',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
@@ -347,71 +312,6 @@ def upgrade() -> None:
op.create_index(op.f('ix_supplier_scorecards_period_start'), 'supplier_scorecards', ['period_start'], unique=False)
op.create_index(op.f('ix_supplier_scorecards_supplier_id'), 'supplier_scorecards', ['supplier_id'], unique=False)
op.create_index(op.f('ix_supplier_scorecards_tenant_id'), 'supplier_scorecards', ['tenant_id'], unique=False)
op.create_table('deliveries',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('purchase_order_id', sa.UUID(), nullable=False),
sa.Column('supplier_id', sa.UUID(), nullable=False),
sa.Column('delivery_number', sa.String(length=50), nullable=False),
sa.Column('supplier_delivery_note', sa.String(length=100), nullable=True),
sa.Column('status', sa.Enum('scheduled', 'in_transit', 'out_for_delivery', 'delivered', 'partially_delivered', 'failed_delivery', 'returned', name='deliverystatus'), nullable=False),
sa.Column('scheduled_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('estimated_arrival', sa.DateTime(timezone=True), nullable=True),
sa.Column('actual_arrival', sa.DateTime(timezone=True), nullable=True),
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('delivery_address', sa.Text(), nullable=True),
sa.Column('delivery_contact', sa.String(length=200), nullable=True),
sa.Column('delivery_phone', sa.String(length=30), nullable=True),
sa.Column('carrier_name', sa.String(length=200), nullable=True),
sa.Column('tracking_number', sa.String(length=100), nullable=True),
sa.Column('inspection_passed', sa.Boolean(), nullable=True),
sa.Column('inspection_notes', sa.Text(), nullable=True),
sa.Column('quality_issues', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('received_by', sa.UUID(), nullable=True),
sa.Column('received_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('photos', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_by', sa.UUID(), nullable=False),
sa.ForeignKeyConstraint(['purchase_order_id'], ['purchase_orders.id'], ),
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_deliveries_delivery_number', 'deliveries', ['delivery_number'], unique=False)
op.create_index(op.f('ix_deliveries_purchase_order_id'), 'deliveries', ['purchase_order_id'], unique=False)
op.create_index('ix_deliveries_scheduled_date', 'deliveries', ['scheduled_date'], unique=False)
op.create_index(op.f('ix_deliveries_status'), 'deliveries', ['status'], unique=False)
op.create_index(op.f('ix_deliveries_supplier_id'), 'deliveries', ['supplier_id'], unique=False)
op.create_index(op.f('ix_deliveries_tenant_id'), 'deliveries', ['tenant_id'], unique=False)
op.create_index('ix_deliveries_tenant_status', 'deliveries', ['tenant_id', 'status'], unique=False)
op.create_table('purchase_order_items',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('purchase_order_id', sa.UUID(), nullable=False),
sa.Column('price_list_item_id', sa.UUID(), nullable=True),
sa.Column('inventory_product_id', sa.UUID(), nullable=False),
sa.Column('product_code', sa.String(length=100), nullable=True),
sa.Column('ordered_quantity', sa.Integer(), nullable=False),
sa.Column('unit_of_measure', sa.String(length=20), nullable=False),
sa.Column('unit_price', sa.Numeric(precision=10, scale=4), nullable=False),
sa.Column('line_total', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('received_quantity', sa.Integer(), nullable=False),
sa.Column('remaining_quantity', sa.Integer(), nullable=False),
sa.Column('quality_requirements', sa.Text(), nullable=True),
sa.Column('item_notes', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['price_list_item_id'], ['supplier_price_lists.id'], ),
sa.ForeignKeyConstraint(['purchase_order_id'], ['purchase_orders.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_po_items_inventory_product', 'purchase_order_items', ['inventory_product_id'], unique=False)
op.create_index('ix_po_items_tenant_po', 'purchase_order_items', ['tenant_id', 'purchase_order_id'], unique=False)
op.create_index(op.f('ix_purchase_order_items_inventory_product_id'), 'purchase_order_items', ['inventory_product_id'], unique=False)
op.create_index(op.f('ix_purchase_order_items_price_list_item_id'), 'purchase_order_items', ['price_list_item_id'], unique=False)
op.create_index(op.f('ix_purchase_order_items_purchase_order_id'), 'purchase_order_items', ['purchase_order_id'], unique=False)
op.create_index(op.f('ix_purchase_order_items_tenant_id'), 'purchase_order_items', ['tenant_id'], unique=False)
op.create_table('supplier_alerts',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
@@ -466,75 +366,6 @@ def upgrade() -> None:
op.create_index(op.f('ix_supplier_alerts_tenant_id'), 'supplier_alerts', ['tenant_id'], unique=False)
op.create_index('ix_supplier_alerts_tenant_supplier', 'supplier_alerts', ['tenant_id', 'supplier_id'], unique=False)
op.create_index('ix_supplier_alerts_type_severity', 'supplier_alerts', ['alert_type', 'severity'], unique=False)
op.create_table('supplier_invoices',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('supplier_id', sa.UUID(), nullable=False),
sa.Column('purchase_order_id', sa.UUID(), nullable=True),
sa.Column('invoice_number', sa.String(length=50), nullable=False),
sa.Column('supplier_invoice_number', sa.String(length=100), nullable=False),
sa.Column('status', sa.Enum('pending', 'approved', 'paid', 'overdue', 'disputed', 'cancelled', name='invoicestatus'), nullable=False),
sa.Column('invoice_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('due_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('received_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('subtotal', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('tax_amount', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('shipping_cost', sa.Numeric(precision=10, scale=2), nullable=False),
sa.Column('discount_amount', sa.Numeric(precision=10, scale=2), nullable=False),
sa.Column('total_amount', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('currency', sa.String(length=3), nullable=False),
sa.Column('paid_amount', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('payment_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('payment_reference', sa.String(length=100), nullable=True),
sa.Column('approved_by', sa.UUID(), nullable=True),
sa.Column('approved_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('rejection_reason', sa.Text(), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('invoice_document_url', sa.String(length=500), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_by', sa.UUID(), nullable=False),
sa.ForeignKeyConstraint(['purchase_order_id'], ['purchase_orders.id'], ),
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_invoices_due_date', 'supplier_invoices', ['due_date'], unique=False)
op.create_index('ix_invoices_invoice_number', 'supplier_invoices', ['invoice_number'], unique=False)
op.create_index('ix_invoices_tenant_status', 'supplier_invoices', ['tenant_id', 'status'], unique=False)
op.create_index('ix_invoices_tenant_supplier', 'supplier_invoices', ['tenant_id', 'supplier_id'], unique=False)
op.create_index(op.f('ix_supplier_invoices_invoice_number'), 'supplier_invoices', ['invoice_number'], unique=False)
op.create_index(op.f('ix_supplier_invoices_purchase_order_id'), 'supplier_invoices', ['purchase_order_id'], unique=False)
op.create_index(op.f('ix_supplier_invoices_status'), 'supplier_invoices', ['status'], unique=False)
op.create_index(op.f('ix_supplier_invoices_supplier_id'), 'supplier_invoices', ['supplier_id'], unique=False)
op.create_index(op.f('ix_supplier_invoices_tenant_id'), 'supplier_invoices', ['tenant_id'], unique=False)
op.create_table('delivery_items',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
sa.Column('delivery_id', sa.UUID(), nullable=False),
sa.Column('purchase_order_item_id', sa.UUID(), nullable=False),
sa.Column('inventory_product_id', sa.UUID(), nullable=False),
sa.Column('ordered_quantity', sa.Integer(), nullable=False),
sa.Column('delivered_quantity', sa.Integer(), nullable=False),
sa.Column('accepted_quantity', sa.Integer(), nullable=False),
sa.Column('rejected_quantity', sa.Integer(), nullable=False),
sa.Column('batch_lot_number', sa.String(length=100), nullable=True),
sa.Column('expiry_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('quality_grade', sa.String(length=20), nullable=True),
sa.Column('quality_issues', sa.Text(), nullable=True),
sa.Column('rejection_reason', sa.Text(), nullable=True),
sa.Column('item_notes', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['delivery_id'], ['deliveries.id'], ),
sa.ForeignKeyConstraint(['purchase_order_item_id'], ['purchase_order_items.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_delivery_items_delivery_id'), 'delivery_items', ['delivery_id'], unique=False)
op.create_index('ix_delivery_items_inventory_product', 'delivery_items', ['inventory_product_id'], unique=False)
op.create_index(op.f('ix_delivery_items_inventory_product_id'), 'delivery_items', ['inventory_product_id'], unique=False)
op.create_index(op.f('ix_delivery_items_purchase_order_item_id'), 'delivery_items', ['purchase_order_item_id'], unique=False)
op.create_index('ix_delivery_items_tenant_delivery', 'delivery_items', ['tenant_id', 'delivery_id'], unique=False)
op.create_index(op.f('ix_delivery_items_tenant_id'), 'delivery_items', ['tenant_id'], unique=False)
op.create_table('supplier_quality_reviews',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
@@ -559,8 +390,6 @@ def upgrade() -> None:
sa.Column('approved_by', sa.UUID(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('reviewed_by', sa.UUID(), nullable=False),
sa.ForeignKeyConstraint(['delivery_id'], ['deliveries.id'], ),
sa.ForeignKeyConstraint(['purchase_order_id'], ['purchase_orders.id'], ),
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'], ),
sa.PrimaryKeyConstraint('id')
)
@@ -571,6 +400,35 @@ def upgrade() -> None:
op.create_index(op.f('ix_supplier_quality_reviews_purchase_order_id'), 'supplier_quality_reviews', ['purchase_order_id'], unique=False)
op.create_index(op.f('ix_supplier_quality_reviews_supplier_id'), 'supplier_quality_reviews', ['supplier_id'], unique=False)
op.create_index(op.f('ix_supplier_quality_reviews_tenant_id'), 'supplier_quality_reviews', ['tenant_id'], unique=False)
# Initialize trust scores for existing suppliers based on their ratings and history
op.execute("""
UPDATE suppliers
SET
trust_score = LEAST(1.0, GREATEST(0.0,
(COALESCE(quality_rating, 0) / 5.0 * 0.4) +
(COALESCE(delivery_rating, 0) / 5.0 * 0.4) +
(CASE WHEN total_orders > 10 THEN 0.2 ELSE total_orders / 50.0 END)
)),
is_preferred_supplier = (
total_orders >= 10 AND
quality_rating >= 4.0 AND
delivery_rating >= 4.0 AND
status = 'active'
),
auto_approve_enabled = (
total_orders >= 20 AND
quality_rating >= 4.5 AND
delivery_rating >= 4.5 AND
status = 'active'
),
total_pos_count = total_orders,
approved_pos_count = total_orders,
on_time_delivery_rate = COALESCE(delivery_rating / 5.0, 0.0),
fulfillment_rate = COALESCE(quality_rating / 5.0, 0.0),
last_performance_update = NOW()
WHERE status = 'active'
""")
# ### end Alembic commands ###
@@ -584,23 +442,6 @@ def downgrade() -> None:
op.drop_index('ix_quality_reviews_overall_rating', table_name='supplier_quality_reviews')
op.drop_index('ix_quality_reviews_date', table_name='supplier_quality_reviews')
op.drop_table('supplier_quality_reviews')
op.drop_index(op.f('ix_delivery_items_tenant_id'), table_name='delivery_items')
op.drop_index('ix_delivery_items_tenant_delivery', table_name='delivery_items')
op.drop_index(op.f('ix_delivery_items_purchase_order_item_id'), table_name='delivery_items')
op.drop_index(op.f('ix_delivery_items_inventory_product_id'), table_name='delivery_items')
op.drop_index('ix_delivery_items_inventory_product', table_name='delivery_items')
op.drop_index(op.f('ix_delivery_items_delivery_id'), table_name='delivery_items')
op.drop_table('delivery_items')
op.drop_index(op.f('ix_supplier_invoices_tenant_id'), table_name='supplier_invoices')
op.drop_index(op.f('ix_supplier_invoices_supplier_id'), table_name='supplier_invoices')
op.drop_index(op.f('ix_supplier_invoices_status'), table_name='supplier_invoices')
op.drop_index(op.f('ix_supplier_invoices_purchase_order_id'), table_name='supplier_invoices')
op.drop_index(op.f('ix_supplier_invoices_invoice_number'), table_name='supplier_invoices')
op.drop_index('ix_invoices_tenant_supplier', table_name='supplier_invoices')
op.drop_index('ix_invoices_tenant_status', table_name='supplier_invoices')
op.drop_index('ix_invoices_invoice_number', table_name='supplier_invoices')
op.drop_index('ix_invoices_due_date', table_name='supplier_invoices')
op.drop_table('supplier_invoices')
op.drop_index('ix_supplier_alerts_type_severity', table_name='supplier_alerts')
op.drop_index('ix_supplier_alerts_tenant_supplier', table_name='supplier_alerts')
op.drop_index(op.f('ix_supplier_alerts_tenant_id'), table_name='supplier_alerts')
@@ -614,21 +455,6 @@ def downgrade() -> None:
op.drop_index(op.f('ix_supplier_alerts_delivery_id'), table_name='supplier_alerts')
op.drop_index(op.f('ix_supplier_alerts_alert_type'), table_name='supplier_alerts')
op.drop_table('supplier_alerts')
op.drop_index(op.f('ix_purchase_order_items_tenant_id'), table_name='purchase_order_items')
op.drop_index(op.f('ix_purchase_order_items_purchase_order_id'), table_name='purchase_order_items')
op.drop_index(op.f('ix_purchase_order_items_price_list_item_id'), table_name='purchase_order_items')
op.drop_index(op.f('ix_purchase_order_items_inventory_product_id'), table_name='purchase_order_items')
op.drop_index('ix_po_items_tenant_po', table_name='purchase_order_items')
op.drop_index('ix_po_items_inventory_product', table_name='purchase_order_items')
op.drop_table('purchase_order_items')
op.drop_index('ix_deliveries_tenant_status', table_name='deliveries')
op.drop_index(op.f('ix_deliveries_tenant_id'), table_name='deliveries')
op.drop_index(op.f('ix_deliveries_supplier_id'), table_name='deliveries')
op.drop_index(op.f('ix_deliveries_status'), table_name='deliveries')
op.drop_index('ix_deliveries_scheduled_date', table_name='deliveries')
op.drop_index(op.f('ix_deliveries_purchase_order_id'), table_name='deliveries')
op.drop_index('ix_deliveries_delivery_number', table_name='deliveries')
op.drop_table('deliveries')
op.drop_index(op.f('ix_supplier_scorecards_tenant_id'), table_name='supplier_scorecards')
op.drop_index(op.f('ix_supplier_scorecards_supplier_id'), table_name='supplier_scorecards')
op.drop_index(op.f('ix_supplier_scorecards_period_start'), table_name='supplier_scorecards')
@@ -643,7 +469,7 @@ def downgrade() -> None:
op.drop_index(op.f('ix_supplier_price_lists_tenant_id'), table_name='supplier_price_lists')
op.drop_index(op.f('ix_supplier_price_lists_supplier_id'), table_name='supplier_price_lists')
op.drop_index(op.f('ix_supplier_price_lists_inventory_product_id'), table_name='supplier_price_lists')
op.drop_index('ix_price_lists_tenant_supplier', table_name='supplier_price_lists')
op.create_index('ix_price_lists_tenant_supplier', 'supplier_price_lists', ['tenant_id', 'supplier_id'], unique=False)
op.drop_index('ix_price_lists_inventory_product', table_name='supplier_price_lists')
op.drop_index('ix_price_lists_effective_date', table_name='supplier_price_lists')
op.drop_index('ix_price_lists_active', table_name='supplier_price_lists')
@@ -659,15 +485,9 @@ def downgrade() -> None:
op.drop_index('ix_performance_metrics_tenant_supplier', table_name='supplier_performance_metrics')
op.drop_index('ix_performance_metrics_period_dates', table_name='supplier_performance_metrics')
op.drop_table('supplier_performance_metrics')
op.drop_index('ix_purchase_orders_tenant_supplier', table_name='purchase_orders')
op.drop_index('ix_purchase_orders_tenant_status', table_name='purchase_orders')
op.drop_index(op.f('ix_purchase_orders_tenant_id'), table_name='purchase_orders')
op.drop_index(op.f('ix_purchase_orders_supplier_id'), table_name='purchase_orders')
op.drop_index(op.f('ix_purchase_orders_status'), table_name='purchase_orders')
op.drop_index('ix_purchase_orders_po_number', table_name='purchase_orders')
op.drop_index('ix_purchase_orders_order_date', table_name='purchase_orders')
op.drop_index('ix_purchase_orders_delivery_date', table_name='purchase_orders')
op.drop_table('purchase_orders')
op.drop_index('ix_suppliers_auto_approve', table_name='suppliers')
op.drop_index('ix_suppliers_preferred', table_name='suppliers')
op.drop_index('ix_suppliers_trust_score', table_name='suppliers')
op.drop_index('ix_suppliers_tenant_type', table_name='suppliers')
op.drop_index('ix_suppliers_tenant_status', table_name='suppliers')
op.drop_index('ix_suppliers_tenant_name', table_name='suppliers')

View File

@@ -1,84 +0,0 @@
"""add_supplier_trust_metrics
Revision ID: add_supplier_trust_metrics
Revises: 93d6ea3dc888
Create Date: 2025-10-20 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'add_supplier_trust_metrics'
down_revision = '93d6ea3dc888'
branch_labels = None
depends_on = None
def upgrade() -> None:
"""Add trust and auto-approval metrics to suppliers table"""
# Add trust and auto-approval metric columns
op.add_column('suppliers', sa.Column('trust_score', sa.Float(), nullable=False, server_default='0.0'))
op.add_column('suppliers', sa.Column('is_preferred_supplier', sa.Boolean(), nullable=False, server_default='false'))
op.add_column('suppliers', sa.Column('auto_approve_enabled', sa.Boolean(), nullable=False, server_default='false'))
op.add_column('suppliers', sa.Column('total_pos_count', sa.Integer(), nullable=False, server_default='0'))
op.add_column('suppliers', sa.Column('approved_pos_count', sa.Integer(), nullable=False, server_default='0'))
op.add_column('suppliers', sa.Column('on_time_delivery_rate', sa.Float(), nullable=False, server_default='0.0'))
op.add_column('suppliers', sa.Column('fulfillment_rate', sa.Float(), nullable=False, server_default='0.0'))
op.add_column('suppliers', sa.Column('last_performance_update', sa.DateTime(timezone=True), nullable=True))
# Create index for trust score queries
op.create_index('ix_suppliers_trust_score', 'suppliers', ['trust_score'], unique=False)
op.create_index('ix_suppliers_preferred', 'suppliers', ['is_preferred_supplier'], unique=False)
op.create_index('ix_suppliers_auto_approve', 'suppliers', ['auto_approve_enabled'], unique=False)
# Update existing active suppliers to have reasonable default trust scores
# Suppliers with good ratings and history get higher initial trust
op.execute("""
UPDATE suppliers
SET
trust_score = LEAST(1.0, GREATEST(0.0,
(COALESCE(quality_rating, 0) / 5.0 * 0.4) +
(COALESCE(delivery_rating, 0) / 5.0 * 0.4) +
(CASE WHEN total_orders > 10 THEN 0.2 ELSE total_orders / 50.0 END)
)),
is_preferred_supplier = (
total_orders >= 10 AND
quality_rating >= 4.0 AND
delivery_rating >= 4.0 AND
status = 'active'
),
auto_approve_enabled = (
total_orders >= 20 AND
quality_rating >= 4.5 AND
delivery_rating >= 4.5 AND
status = 'active'
),
total_pos_count = total_orders,
approved_pos_count = total_orders,
on_time_delivery_rate = COALESCE(delivery_rating / 5.0, 0.0),
fulfillment_rate = COALESCE(quality_rating / 5.0, 0.0),
last_performance_update = NOW()
WHERE status = 'active'
""")
def downgrade() -> None:
"""Remove trust and auto-approval metrics from suppliers table"""
# Drop indexes
op.drop_index('ix_suppliers_auto_approve', table_name='suppliers')
op.drop_index('ix_suppliers_preferred', table_name='suppliers')
op.drop_index('ix_suppliers_trust_score', table_name='suppliers')
# Drop columns
op.drop_column('suppliers', 'last_performance_update')
op.drop_column('suppliers', 'fulfillment_rate')
op.drop_column('suppliers', 'on_time_delivery_rate')
op.drop_column('suppliers', 'approved_pos_count')
op.drop_column('suppliers', 'total_pos_count')
op.drop_column('suppliers', 'auto_approve_enabled')
op.drop_column('suppliers', 'is_preferred_supplier')
op.drop_column('suppliers', 'trust_score')

View File

@@ -1,463 +0,0 @@
#!/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)