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'