Initial commit - production deployment
This commit is contained in:
38
services/procurement/app/models/__init__.py
Normal file
38
services/procurement/app/models/__init__.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# ================================================================
|
||||
# services/procurement/app/models/__init__.py
|
||||
# ================================================================
|
||||
"""
|
||||
Procurement Service Models
|
||||
"""
|
||||
|
||||
from .procurement_plan import ProcurementPlan, ProcurementRequirement
|
||||
from .purchase_order import (
|
||||
PurchaseOrder,
|
||||
PurchaseOrderItem,
|
||||
PurchaseOrderStatus,
|
||||
Delivery,
|
||||
DeliveryItem,
|
||||
DeliveryStatus,
|
||||
SupplierInvoice,
|
||||
InvoiceStatus,
|
||||
QualityRating,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Procurement Planning
|
||||
"ProcurementPlan",
|
||||
"ProcurementRequirement",
|
||||
# Purchase Orders
|
||||
"PurchaseOrder",
|
||||
"PurchaseOrderItem",
|
||||
"PurchaseOrderStatus",
|
||||
# Deliveries
|
||||
"Delivery",
|
||||
"DeliveryItem",
|
||||
"DeliveryStatus",
|
||||
# Invoices
|
||||
"SupplierInvoice",
|
||||
"InvoiceStatus",
|
||||
# Enums
|
||||
"QualityRating",
|
||||
]
|
||||
234
services/procurement/app/models/procurement_plan.py
Normal file
234
services/procurement/app/models/procurement_plan.py
Normal file
@@ -0,0 +1,234 @@
|
||||
# ================================================================
|
||||
# services/procurement/app/models/procurement_plan.py
|
||||
# ================================================================
|
||||
"""
|
||||
Procurement Planning Models
|
||||
Migrated from Orders Service
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, Date, Numeric, Text, Integer, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from shared.database.base import Base
|
||||
|
||||
|
||||
class ProcurementPlan(Base):
|
||||
"""Master procurement plan for coordinating supply needs across orders and production"""
|
||||
__tablename__ = "procurement_plans"
|
||||
|
||||
# Primary identification
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
plan_number = Column(String(50), nullable=False, unique=True, index=True)
|
||||
|
||||
# Plan scope and timing
|
||||
plan_date = Column(Date, nullable=False, index=True)
|
||||
plan_period_start = Column(Date, nullable=False)
|
||||
plan_period_end = Column(Date, nullable=False)
|
||||
planning_horizon_days = Column(Integer, nullable=False, default=14)
|
||||
|
||||
# Plan status and lifecycle
|
||||
status = Column(String(50), nullable=False, default="draft", index=True)
|
||||
# Status values: draft, pending_approval, approved, in_execution, completed, cancelled
|
||||
|
||||
plan_type = Column(String(50), nullable=False, default="regular") # regular, emergency, seasonal
|
||||
priority = Column(String(20), nullable=False, default="normal") # high, normal, low
|
||||
|
||||
# Business model context
|
||||
business_model = Column(String(50), nullable=True) # individual_bakery, central_bakery
|
||||
procurement_strategy = Column(String(50), nullable=False, default="just_in_time") # just_in_time, bulk, mixed
|
||||
|
||||
# Plan totals and summary
|
||||
total_requirements = Column(Integer, nullable=False, default=0)
|
||||
total_estimated_cost = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00"))
|
||||
total_approved_cost = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00"))
|
||||
cost_variance = Column(Numeric(12, 2), nullable=False, default=Decimal("0.00"))
|
||||
|
||||
# Demand analysis
|
||||
total_demand_orders = Column(Integer, nullable=False, default=0)
|
||||
total_demand_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
|
||||
total_production_requirements = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
|
||||
safety_stock_buffer = Column(Numeric(5, 2), nullable=False, default=Decimal("20.00")) # Percentage
|
||||
|
||||
# Supplier coordination
|
||||
primary_suppliers_count = Column(Integer, nullable=False, default=0)
|
||||
backup_suppliers_count = Column(Integer, nullable=False, default=0)
|
||||
supplier_diversification_score = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0
|
||||
|
||||
# Risk assessment
|
||||
supply_risk_level = Column(String(20), nullable=False, default="low") # low, medium, high, critical
|
||||
demand_forecast_confidence = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0
|
||||
seasonality_adjustment = Column(Numeric(5, 2), nullable=False, default=Decimal("0.00"))
|
||||
|
||||
# Execution tracking
|
||||
approved_at = Column(DateTime(timezone=True), nullable=True)
|
||||
approved_by = Column(UUID(as_uuid=True), nullable=True)
|
||||
execution_started_at = Column(DateTime(timezone=True), nullable=True)
|
||||
execution_completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Performance metrics
|
||||
fulfillment_rate = Column(Numeric(5, 2), nullable=True) # Percentage
|
||||
on_time_delivery_rate = Column(Numeric(5, 2), nullable=True) # Percentage
|
||||
cost_accuracy = Column(Numeric(5, 2), nullable=True) # Percentage
|
||||
quality_score = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0
|
||||
|
||||
# Integration data
|
||||
source_orders = Column(JSONB, nullable=True) # Orders that drove this plan
|
||||
production_schedules = Column(JSONB, nullable=True) # Associated production schedules
|
||||
inventory_snapshots = Column(JSONB, nullable=True) # Inventory levels at planning time
|
||||
forecast_data = Column(JSONB, nullable=True) # Forecasting service data used for this plan
|
||||
|
||||
# Communication and collaboration
|
||||
stakeholder_notifications = Column(JSONB, nullable=True) # Who was notified and when
|
||||
approval_workflow = Column(JSONB, nullable=True) # Approval chain and status
|
||||
|
||||
# Special considerations
|
||||
special_requirements = Column(Text, nullable=True)
|
||||
seasonal_adjustments = Column(JSONB, nullable=True)
|
||||
emergency_provisions = Column(JSONB, nullable=True)
|
||||
|
||||
# External references
|
||||
erp_reference = Column(String(100), nullable=True)
|
||||
supplier_portal_reference = Column(String(100), 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=True)
|
||||
updated_by = Column(UUID(as_uuid=True), nullable=True)
|
||||
|
||||
# Additional metadata
|
||||
plan_metadata = Column(JSONB, nullable=True)
|
||||
|
||||
# Relationships
|
||||
requirements = relationship("ProcurementRequirement", back_populates="plan", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class ProcurementRequirement(Base):
|
||||
"""Individual procurement requirements within a procurement plan"""
|
||||
__tablename__ = "procurement_requirements"
|
||||
|
||||
# Primary identification
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
plan_id = Column(UUID(as_uuid=True), ForeignKey("procurement_plans.id", ondelete="CASCADE"), nullable=False)
|
||||
requirement_number = Column(String(50), nullable=False, index=True)
|
||||
|
||||
# Product/ingredient information
|
||||
product_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Reference to products/ingredients
|
||||
product_name = Column(String(200), nullable=False)
|
||||
product_sku = Column(String(100), nullable=True)
|
||||
product_category = Column(String(100), nullable=True)
|
||||
product_type = Column(String(50), nullable=False, default="ingredient") # ingredient, packaging, supplies
|
||||
|
||||
# Local production tracking
|
||||
is_locally_produced = Column(Boolean, nullable=False, default=False) # If true, this is for a locally-produced item
|
||||
recipe_id = Column(UUID(as_uuid=True), nullable=True) # Recipe used for BOM explosion
|
||||
parent_requirement_id = Column(UUID(as_uuid=True), nullable=True) # If this is from BOM explosion
|
||||
bom_explosion_level = Column(Integer, nullable=False, default=0) # Depth in BOM tree
|
||||
|
||||
# Requirement details
|
||||
required_quantity = Column(Numeric(12, 3), nullable=False)
|
||||
unit_of_measure = Column(String(50), nullable=False)
|
||||
safety_stock_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
|
||||
total_quantity_needed = Column(Numeric(12, 3), nullable=False)
|
||||
|
||||
# Current inventory situation
|
||||
current_stock_level = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
|
||||
reserved_stock = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
|
||||
available_stock = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
|
||||
net_requirement = Column(Numeric(12, 3), nullable=False)
|
||||
|
||||
# Demand breakdown
|
||||
order_demand = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
|
||||
production_demand = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
|
||||
forecast_demand = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
|
||||
buffer_demand = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
|
||||
|
||||
# Supplier information
|
||||
preferred_supplier_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
backup_supplier_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
supplier_name = Column(String(200), nullable=True)
|
||||
supplier_lead_time_days = Column(Integer, nullable=True)
|
||||
minimum_order_quantity = Column(Numeric(12, 3), nullable=True)
|
||||
|
||||
# Pricing and cost
|
||||
estimated_unit_cost = Column(Numeric(10, 4), nullable=True)
|
||||
estimated_total_cost = Column(Numeric(12, 2), nullable=True)
|
||||
last_purchase_cost = Column(Numeric(10, 4), nullable=True)
|
||||
cost_variance = Column(Numeric(10, 2), nullable=False, default=Decimal("0.00"))
|
||||
|
||||
# Timing requirements
|
||||
required_by_date = Column(Date, nullable=False)
|
||||
lead_time_buffer_days = Column(Integer, nullable=False, default=1)
|
||||
suggested_order_date = Column(Date, nullable=False)
|
||||
latest_order_date = Column(Date, nullable=False)
|
||||
|
||||
# Quality and specifications
|
||||
quality_specifications = Column(JSONB, nullable=True)
|
||||
special_requirements = Column(Text, nullable=True)
|
||||
storage_requirements = Column(String(200), nullable=True)
|
||||
shelf_life_days = Column(Integer, nullable=True)
|
||||
|
||||
# Requirement status
|
||||
status = Column(String(50), nullable=False, default="pending")
|
||||
# Status values: pending, approved, ordered, partially_received, received, cancelled
|
||||
|
||||
priority = Column(String(20), nullable=False, default="normal") # critical, high, normal, low
|
||||
risk_level = Column(String(20), nullable=False, default="low") # low, medium, high, critical
|
||||
|
||||
# Purchase order tracking
|
||||
purchase_order_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
purchase_order_number = Column(String(50), nullable=True)
|
||||
ordered_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
|
||||
ordered_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Delivery tracking
|
||||
expected_delivery_date = Column(Date, nullable=True)
|
||||
actual_delivery_date = Column(Date, nullable=True)
|
||||
received_quantity = Column(Numeric(12, 3), nullable=False, default=Decimal("0.000"))
|
||||
delivery_status = Column(String(50), nullable=False, default="pending")
|
||||
|
||||
# Performance tracking
|
||||
fulfillment_rate = Column(Numeric(5, 2), nullable=True) # Percentage
|
||||
on_time_delivery = Column(Boolean, nullable=True)
|
||||
quality_rating = Column(Numeric(3, 1), nullable=True) # 1.0 to 10.0
|
||||
|
||||
# Source traceability
|
||||
source_orders = Column(JSONB, nullable=True) # Orders that contributed to this requirement
|
||||
source_production_batches = Column(JSONB, nullable=True) # Production batches needing this
|
||||
demand_analysis = Column(JSONB, nullable=True) # Detailed demand breakdown
|
||||
|
||||
# Smart procurement calculation metadata
|
||||
calculation_method = Column(String(100), nullable=True) # Method used: REORDER_POINT_TRIGGERED, FORECAST_DRIVEN_PROACTIVE, etc.
|
||||
ai_suggested_quantity = Column(Numeric(12, 3), nullable=True) # Pure AI forecast quantity
|
||||
adjusted_quantity = Column(Numeric(12, 3), nullable=True) # Final quantity after applying constraints
|
||||
adjustment_reason = Column(Text, nullable=True) # Human-readable explanation of adjustments
|
||||
price_tier_applied = Column(JSONB, nullable=True) # Price tier information if applicable
|
||||
supplier_minimum_applied = Column(Boolean, nullable=False, default=False) # Whether supplier minimum was enforced
|
||||
storage_limit_applied = Column(Boolean, nullable=False, default=False) # Whether storage limit was hit
|
||||
reorder_rule_applied = Column(Boolean, nullable=False, default=False) # Whether reorder rules were used
|
||||
|
||||
# Approval and authorization
|
||||
approved_quantity = Column(Numeric(12, 3), nullable=True)
|
||||
approved_cost = Column(Numeric(12, 2), nullable=True)
|
||||
approved_at = Column(DateTime(timezone=True), nullable=True)
|
||||
approved_by = Column(UUID(as_uuid=True), nullable=True)
|
||||
|
||||
# Notes and communication
|
||||
procurement_notes = Column(Text, nullable=True)
|
||||
supplier_communication = Column(JSONB, 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)
|
||||
|
||||
# Additional metadata
|
||||
requirement_metadata = Column(JSONB, nullable=True)
|
||||
|
||||
# Relationships
|
||||
plan = relationship("ProcurementPlan", back_populates="requirements")
|
||||
381
services/procurement/app/models/purchase_order.py
Normal file
381
services/procurement/app/models/purchase_order.py
Normal file
@@ -0,0 +1,381 @@
|
||||
# ================================================================
|
||||
# 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'),
|
||||
)
|
||||
194
services/procurement/app/models/replenishment.py
Normal file
194
services/procurement/app/models/replenishment.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Database models for replenishment planning.
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, Integer, Numeric, Date, Boolean, ForeignKey, Text, TIMESTAMP, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from sqlalchemy.orm import relationship
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from shared.database import Base
|
||||
|
||||
|
||||
class ReplenishmentPlan(Base):
|
||||
"""Replenishment plan master record"""
|
||||
__tablename__ = "replenishment_plans"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Planning metadata
|
||||
planning_date = Column(Date, nullable=False)
|
||||
projection_horizon_days = Column(Integer, nullable=False, default=7)
|
||||
|
||||
# References
|
||||
forecast_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
production_schedule_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
|
||||
# Summary statistics
|
||||
total_items = Column(Integer, nullable=False, default=0)
|
||||
urgent_items = Column(Integer, nullable=False, default=0)
|
||||
high_risk_items = Column(Integer, nullable=False, default=0)
|
||||
total_estimated_cost = Column(Numeric(12, 2), nullable=False, default=0)
|
||||
|
||||
# Status
|
||||
status = Column(String(50), nullable=False, default='draft') # draft, approved, executed
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow)
|
||||
updated_at = Column(TIMESTAMP(timezone=True), nullable=True, onupdate=datetime.utcnow)
|
||||
executed_at = Column(TIMESTAMP(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
items = relationship("ReplenishmentPlanItem", back_populates="plan", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class ReplenishmentPlanItem(Base):
|
||||
"""Individual item in a replenishment plan"""
|
||||
__tablename__ = "replenishment_plan_items"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
replenishment_plan_id = Column(UUID(as_uuid=True), ForeignKey("replenishment_plans.id"), nullable=False, index=True)
|
||||
|
||||
# Ingredient info
|
||||
ingredient_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
ingredient_name = Column(String(200), nullable=False)
|
||||
unit_of_measure = Column(String(20), nullable=False)
|
||||
|
||||
# Quantities
|
||||
base_quantity = Column(Numeric(12, 3), nullable=False)
|
||||
safety_stock_quantity = Column(Numeric(12, 3), nullable=False, default=0)
|
||||
shelf_life_adjusted_quantity = Column(Numeric(12, 3), nullable=False)
|
||||
final_order_quantity = Column(Numeric(12, 3), nullable=False)
|
||||
|
||||
# Dates
|
||||
order_date = Column(Date, nullable=False, index=True)
|
||||
delivery_date = Column(Date, nullable=False)
|
||||
required_by_date = Column(Date, nullable=False)
|
||||
|
||||
# Planning metadata
|
||||
lead_time_days = Column(Integer, nullable=False)
|
||||
is_urgent = Column(Boolean, nullable=False, default=False, index=True)
|
||||
urgency_reason = Column(Text, nullable=True)
|
||||
waste_risk = Column(String(20), nullable=False, default='low') # low, medium, high
|
||||
stockout_risk = Column(String(20), nullable=False, default='low') # low, medium, high, critical
|
||||
|
||||
# Supplier
|
||||
supplier_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
|
||||
# Calculation details (stored as JSONB)
|
||||
safety_stock_calculation = Column(JSONB, nullable=True)
|
||||
shelf_life_adjustment = Column(JSONB, nullable=True)
|
||||
inventory_projection = Column(JSONB, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
plan = relationship("ReplenishmentPlan", back_populates="items")
|
||||
|
||||
|
||||
class InventoryProjection(Base):
|
||||
"""Daily inventory projection"""
|
||||
__tablename__ = "inventory_projections"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Ingredient
|
||||
ingredient_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
ingredient_name = Column(String(200), nullable=False)
|
||||
|
||||
# Projection date
|
||||
projection_date = Column(Date, nullable=False, index=True)
|
||||
|
||||
# Stock levels
|
||||
starting_stock = Column(Numeric(12, 3), nullable=False)
|
||||
forecasted_consumption = Column(Numeric(12, 3), nullable=False, default=0)
|
||||
scheduled_receipts = Column(Numeric(12, 3), nullable=False, default=0)
|
||||
projected_ending_stock = Column(Numeric(12, 3), nullable=False)
|
||||
|
||||
# Flags
|
||||
is_stockout = Column(Boolean, nullable=False, default=False, index=True)
|
||||
coverage_gap = Column(Numeric(12, 3), nullable=False, default=0) # Negative if stockout
|
||||
|
||||
# Reference to replenishment plan
|
||||
replenishment_plan_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
# Unique constraint: one projection per ingredient per date per tenant
|
||||
{'schema': None}
|
||||
)
|
||||
|
||||
|
||||
class SupplierAllocation(Base):
|
||||
"""Supplier allocation for a requirement"""
|
||||
__tablename__ = "supplier_allocations"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
|
||||
# References
|
||||
replenishment_plan_item_id = Column(UUID(as_uuid=True), ForeignKey("replenishment_plan_items.id"), nullable=True, index=True)
|
||||
requirement_id = Column(UUID(as_uuid=True), nullable=True, index=True) # Reference to procurement_requirements
|
||||
|
||||
# Supplier
|
||||
supplier_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
supplier_name = Column(String(200), nullable=False)
|
||||
|
||||
# Allocation
|
||||
allocation_type = Column(String(20), nullable=False) # primary, backup, diversification
|
||||
allocated_quantity = Column(Numeric(12, 3), nullable=False)
|
||||
allocation_percentage = Column(Numeric(5, 4), nullable=False) # 0.0000 - 1.0000
|
||||
|
||||
# Pricing
|
||||
unit_price = Column(Numeric(12, 2), nullable=False)
|
||||
total_cost = Column(Numeric(12, 2), nullable=False)
|
||||
|
||||
# Lead time
|
||||
lead_time_days = Column(Integer, nullable=False)
|
||||
|
||||
# Scoring
|
||||
supplier_score = Column(Numeric(5, 2), nullable=False)
|
||||
score_breakdown = Column(JSONB, nullable=True)
|
||||
|
||||
# Reasoning
|
||||
allocation_reason = Column(Text, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow)
|
||||
|
||||
|
||||
class SupplierSelectionHistory(Base):
|
||||
"""Historical record of supplier selections for analytics"""
|
||||
__tablename__ = "supplier_selection_history"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Selection details
|
||||
ingredient_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
ingredient_name = Column(String(200), nullable=False)
|
||||
selected_supplier_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
selected_supplier_name = Column(String(200), nullable=False)
|
||||
|
||||
# Order details
|
||||
selection_date = Column(Date, nullable=False, index=True)
|
||||
quantity = Column(Numeric(12, 3), nullable=False)
|
||||
unit_price = Column(Numeric(12, 2), nullable=False)
|
||||
total_cost = Column(Numeric(12, 2), nullable=False)
|
||||
|
||||
# Metrics
|
||||
lead_time_days = Column(Integer, nullable=False)
|
||||
quality_score = Column(Numeric(5, 2), nullable=True)
|
||||
delivery_performance = Column(Numeric(5, 2), nullable=True)
|
||||
|
||||
# Selection strategy
|
||||
selection_strategy = Column(String(50), nullable=False) # single_source, dual_source, multi_source
|
||||
was_primary_choice = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow)
|
||||
Reference in New Issue
Block a user