Files
bakery-ia/services/procurement/app/models/purchase_order.py

349 lines
15 KiB
Python
Raw Normal View History

2025-10-30 21:08:07 +01:00
# ================================================================
# 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
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)
# 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)
# 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'),
)