# ================================================================ # services/procurement/app/models/purchase_order.py # ================================================================ """ Purchase Order Models Migrated from Suppliers Service - Now owned by Procurement Service """ import uuid import enum from datetime import datetime, timezone from decimal import Decimal 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, deferred from sqlalchemy.sql import func from shared.database.base import Base 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 InvoiceStatus(enum.Enum): """Invoice processing status""" pending = "pending" approved = "approved" paid = "paid" overdue = "overdue" disputed = "disputed" cancelled = "cancelled" class PurchaseOrder(Base): """Purchase orders to suppliers - Core procurement execution""" __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), nullable=False, index=True) # Reference to Suppliers Service # Order identification po_number = Column(String(50), nullable=False, unique=True, index=True) # Human-readable PO number reference_number = Column(String(100), nullable=True) # Internal reference # Link to procurement plan procurement_plan_id = Column(UUID(as_uuid=True), nullable=True, index=True) # Link to ProcurementPlan # 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) # Stored as DateTime for consistency estimated_delivery_date = Column(DateTime(timezone=True), nullable=True) expected_delivery_date = Column(DateTime(timezone=True), nullable=True) # When delivery is actually expected (used for dashboard tracking) # Financial information subtotal = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00")) tax_amount = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00")) shipping_cost = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00")) discount_amount = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00")) total_amount = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00")) 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) # Auto-approval tracking auto_approved = Column(Boolean, nullable=False, default=False) # Whether this was auto-approved auto_approval_rule_id = Column(UUID(as_uuid=True), nullable=True) # Which rule approved it # 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) # JTBD Dashboard: Structured reasoning data for i18n support # Backend stores structured data, frontend translates using i18n reasoning_data = Column(JSONB, nullable=True) # Structured reasoning data for multilingual support # reasoning_data structure (see shared/schemas/reasoning_types.py): # { # "type": "low_stock_detection" | "forecast_demand" | "safety_stock_replenishment" | etc., # "parameters": { # "supplier_name": "Harinas del Norte", # "product_names": ["Flour Type 55", "Flour Type 45"], # "days_until_stockout": 3, # "current_stock": 45.5, # "required_stock": 200 # }, # "consequence": { # "type": "stockout_risk", # "severity": "high", # "impact_days": 3, # "affected_products": ["Baguette", "Croissant"] # }, # "metadata": { # "trigger_source": "orchestrator_auto", # "forecast_confidence": 0.85, # "ai_assisted": true # } # } # Internal transfer fields (for enterprise parent-child transfers) is_internal = Column(Boolean, default=False, nullable=False, index=True) # Flag for internal transfers source_tenant_id = Column(UUID(as_uuid=True), nullable=True, index=True) # Parent tenant for internal transfers destination_tenant_id = Column(UUID(as_uuid=True), nullable=True, index=True) # Child tenant for internal transfers transfer_type = Column(String(50), nullable=True) # finished_goods, raw_materials # Audit fields created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False) created_by = Column(UUID(as_uuid=True), nullable=False) updated_by = Column(UUID(as_uuid=True), nullable=False) # Relationships items = relationship("PurchaseOrderItem", back_populates="purchase_order", cascade="all, delete-orphan") deliveries = relationship("Delivery", back_populates="purchase_order", cascade="all, delete-orphan") invoices = relationship("SupplierInvoice", back_populates="purchase_order", cascade="all, delete-orphan") # 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_tenant_plan', 'tenant_id', 'procurement_plan_id'), 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', ondelete='CASCADE'), nullable=False, index=True) # Link to procurement requirement procurement_requirement_id = Column(UUID(as_uuid=True), nullable=True, index=True) # Link to ProcurementRequirement # Product identification (references Inventory Service) inventory_product_id = Column(UUID(as_uuid=True), nullable=False, index=True) product_code = Column(String(100), nullable=True) # Supplier's product code product_name = Column(String(200), nullable=False) # Denormalized for convenience # Supplier price list reference (from Suppliers Service) supplier_price_list_id = Column(UUID(as_uuid=True), nullable=True, index=True) # Order quantities ordered_quantity = Column(Numeric(12, 3), 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(Numeric(12, 3), nullable=False, default=Decimal("0.000")) remaining_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) # Quality and notes quality_requirements = Column(Text, nullable=True) item_notes = Column(Text, nullable=True) # Audit fields created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False) # Relationships purchase_order = relationship("PurchaseOrder", back_populates="items") delivery_items = relationship("DeliveryItem", back_populates="purchase_order_item", cascade="all, delete-orphan") # Indexes __table_args__ = ( Index('ix_po_items_tenant_po', 'tenant_id', 'purchase_order_id'), Index('ix_po_items_inventory_product', 'inventory_product_id'), Index('ix_po_items_requirement', 'procurement_requirement_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', ondelete='CASCADE'), nullable=False, index=True) supplier_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Reference to Suppliers Service # Delivery identification delivery_number = Column(String(50), nullable=False, unique=True, 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), server_default=func.now(), nullable=False) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False) created_by = Column(UUID(as_uuid=True), nullable=False) # Relationships purchase_order = relationship("PurchaseOrder", back_populates="deliveries") 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_tenant_po', 'tenant_id', 'purchase_order_id'), ) 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', ondelete='CASCADE'), nullable=False, index=True) purchase_order_item_id = Column(UUID(as_uuid=True), ForeignKey('purchase_order_items.id', ondelete='CASCADE'), nullable=False, index=True) # Product identification inventory_product_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Delivery quantities ordered_quantity = Column(Numeric(12, 3), nullable=False) delivered_quantity = Column(Numeric(12, 3), nullable=False) accepted_quantity = Column(Numeric(12, 3), nullable=False) rejected_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000")) # 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), server_default=func.now(), nullable=False) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False) # 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 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), nullable=False, index=True) # Reference to Suppliers Service purchase_order_id = Column(UUID(as_uuid=True), ForeignKey('purchase_orders.id', ondelete='SET NULL'), nullable=True, index=True) # Invoice identification invoice_number = Column(String(50), nullable=False, unique=True, 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=Decimal("0.00")) shipping_cost = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00")) discount_amount = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00")) 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=Decimal("0.00")) 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), server_default=func.now(), nullable=False) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False) created_by = Column(UUID(as_uuid=True), nullable=False) # Relationships 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'), )