# 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""" cod = "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'), )