Files
bakery-ia/services/suppliers/app/models/suppliers.py
2025-08-14 16:47:34 +02:00

562 lines
24 KiB
Python

# services/suppliers/app/models/suppliers.py
"""
Supplier & Procurement management models for Suppliers Service
Comprehensive supplier management, purchase orders, deliveries, and vendor relationships
"""
from sqlalchemy import Column, String, DateTime, Float, Integer, Text, Index, Boolean, Numeric, ForeignKey, Enum as SQLEnum
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
import uuid
import enum
from datetime import datetime, timezone
from typing import Dict, Any, Optional, List
from decimal import Decimal
from shared.database.base import Base
class SupplierType(enum.Enum):
"""Types of suppliers"""
INGREDIENTS = "ingredients" # Raw materials supplier
PACKAGING = "packaging" # Packaging materials
EQUIPMENT = "equipment" # Bakery equipment
SERVICES = "services" # Service providers
UTILITIES = "utilities" # Utilities (gas, electricity)
MULTI = "multi" # Multi-category supplier
class SupplierStatus(enum.Enum):
"""Supplier lifecycle status"""
ACTIVE = "active"
INACTIVE = "inactive"
PENDING_APPROVAL = "pending_approval"
SUSPENDED = "suspended"
BLACKLISTED = "blacklisted"
class PaymentTerms(enum.Enum):
"""Payment terms with suppliers"""
CASH_ON_DELIVERY = "cod"
NET_15 = "net_15"
NET_30 = "net_30"
NET_45 = "net_45"
NET_60 = "net_60"
PREPAID = "prepaid"
CREDIT_TERMS = "credit_terms"
class PurchaseOrderStatus(enum.Enum):
"""Purchase order lifecycle status"""
DRAFT = "draft"
PENDING_APPROVAL = "pending_approval"
APPROVED = "approved"
SENT_TO_SUPPLIER = "sent_to_supplier"
CONFIRMED = "confirmed"
PARTIALLY_RECEIVED = "partially_received"
COMPLETED = "completed"
CANCELLED = "cancelled"
DISPUTED = "disputed"
class DeliveryStatus(enum.Enum):
"""Delivery status tracking"""
SCHEDULED = "scheduled"
IN_TRANSIT = "in_transit"
OUT_FOR_DELIVERY = "out_for_delivery"
DELIVERED = "delivered"
PARTIALLY_DELIVERED = "partially_delivered"
FAILED_DELIVERY = "failed_delivery"
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"""
EXCELLENT = 5
GOOD = 4
AVERAGE = 3
POOR = 2
VERY_POOR = 1
class InvoiceStatus(enum.Enum):
"""Invoice processing status"""
PENDING = "pending"
APPROVED = "approved"
PAID = "paid"
OVERDUE = "overdue"
DISPUTED = "disputed"
CANCELLED = "cancelled"
class Supplier(Base):
"""Master supplier information"""
__tablename__ = "suppliers"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Basic supplier information
name = Column(String(255), nullable=False, index=True)
supplier_code = Column(String(50), nullable=True, index=True) # Internal reference code
tax_id = Column(String(50), nullable=True) # VAT/Tax ID
registration_number = Column(String(100), nullable=True) # Business registration number
# Supplier classification
supplier_type = Column(SQLEnum(SupplierType), nullable=False, index=True)
status = Column(SQLEnum(SupplierStatus), nullable=False, default=SupplierStatus.PENDING_APPROVAL, index=True)
# Contact information
contact_person = Column(String(200), nullable=True)
email = Column(String(254), nullable=True)
phone = Column(String(30), nullable=True)
mobile = Column(String(30), nullable=True)
website = Column(String(255), nullable=True)
# Address information
address_line1 = Column(String(255), nullable=True)
address_line2 = Column(String(255), nullable=True)
city = Column(String(100), nullable=True)
state_province = Column(String(100), nullable=True)
postal_code = Column(String(20), nullable=True)
country = Column(String(100), nullable=True)
# Business terms
payment_terms = Column(SQLEnum(PaymentTerms), nullable=False, default=PaymentTerms.NET_30)
credit_limit = Column(Numeric(12, 2), nullable=True)
currency = Column(String(3), nullable=False, default="EUR") # ISO currency code
# Lead times (in days)
standard_lead_time = Column(Integer, nullable=False, default=3)
minimum_order_amount = Column(Numeric(10, 2), nullable=True)
delivery_area = Column(String(255), nullable=True)
# Quality and performance metrics
quality_rating = Column(Float, nullable=True, default=0.0) # Average quality rating (1-5)
delivery_rating = Column(Float, nullable=True, default=0.0) # Average delivery rating (1-5)
total_orders = Column(Integer, nullable=False, default=0)
total_amount = Column(Numeric(12, 2), nullable=False, default=0.0)
# Onboarding and approval
approved_by = Column(UUID(as_uuid=True), nullable=True) # User who approved
approved_at = Column(DateTime(timezone=True), nullable=True)
rejection_reason = Column(Text, nullable=True)
# Additional information
notes = Column(Text, nullable=True)
certifications = Column(JSONB, nullable=True) # Quality certifications, licenses
business_hours = Column(JSONB, nullable=True) # Operating hours by day
specializations = Column(JSONB, nullable=True) # Product categories, special services
# 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
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
__table_args__ = (
Index('ix_suppliers_tenant_name', 'tenant_id', 'name'),
Index('ix_suppliers_tenant_status', 'tenant_id', 'status'),
Index('ix_suppliers_tenant_type', 'tenant_id', 'supplier_type'),
Index('ix_suppliers_quality_rating', 'quality_rating'),
)
class SupplierPriceList(Base):
"""Product pricing from suppliers"""
__tablename__ = "supplier_price_lists"
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)
# Product identification (references inventory service)
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
# Pricing information
unit_price = Column(Numeric(10, 4), nullable=False)
unit_of_measure = Column(String(20), nullable=False) # kg, g, l, ml, units, etc.
minimum_order_quantity = Column(Integer, nullable=True, default=1)
price_per_unit = Column(Numeric(10, 4), nullable=False) # Calculated field
# Pricing tiers (volume discounts)
tier_pricing = Column(JSONB, nullable=True) # [{quantity: 100, price: 2.50}, ...]
# Validity and terms
effective_date = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
expiry_date = Column(DateTime(timezone=True), nullable=True)
is_active = Column(Boolean, nullable=False, default=True)
# Additional product details
brand = Column(String(100), nullable=True)
packaging_size = Column(String(50), nullable=True)
origin_country = Column(String(100), nullable=True)
shelf_life_days = Column(Integer, nullable=True)
storage_requirements = Column(Text, nullable=True)
# Quality specifications
quality_specs = Column(JSONB, nullable=True) # Quality parameters, certifications
allergens = Column(JSONB, nullable=True) # Allergen information
# 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="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'),
Index('ix_price_lists_inventory_product', 'inventory_product_id'),
Index('ix_price_lists_active', 'is_active'),
Index('ix_price_lists_effective_date', 'effective_date'),
)
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
# Ratings (1-5 scale)
quality_rating = Column(SQLEnum(QualityRating), nullable=False)
delivery_rating = Column(SQLEnum(DeliveryRating), nullable=False)
communication_rating = Column(Integer, nullable=False) # 1-5
overall_rating = Column(Float, nullable=False) # Calculated average
# Detailed feedback
quality_comments = Column(Text, nullable=True)
delivery_comments = Column(Text, nullable=True)
communication_comments = Column(Text, nullable=True)
improvement_suggestions = Column(Text, nullable=True)
# Issues and corrective actions
quality_issues = Column(JSONB, nullable=True) # Documented issues
corrective_actions = Column(Text, nullable=True)
follow_up_required = Column(Boolean, nullable=False, default=False)
follow_up_date = Column(DateTime(timezone=True), nullable=True)
# Review status
is_final = Column(Boolean, nullable=False, default=True)
approved_by = Column(UUID(as_uuid=True), nullable=True)
# Audit fields
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
reviewed_by = Column(UUID(as_uuid=True), nullable=False)
# Relationships
supplier = relationship("Supplier", back_populates="quality_reviews")
# Indexes
__table_args__ = (
Index('ix_quality_reviews_tenant_supplier', 'tenant_id', 'supplier_id'),
Index('ix_quality_reviews_date', 'review_date'),
Index('ix_quality_reviews_overall_rating', 'overall_rating'),
)
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'),
)