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
|
2025-11-07 17:35:38 +00:00
|
|
|
from sqlalchemy.orm import relationship, deferred
|
2025-10-30 21:08:07 +01:00
|
|
|
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)
|
2025-11-27 15:52:40 +01:00
|
|
|
expected_delivery_date = Column(DateTime(timezone=True), nullable=True) # When delivery is actually expected (used for dashboard tracking)
|
2025-10-30 21:08:07 +01:00
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
2025-11-07 18:20:05 +00:00
|
|
|
# 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
|
|
|
|
|
# }
|
feat: Complete JTBD-aligned bakery dashboard redesign
Implements comprehensive dashboard redesign based on Jobs To Be Done methodology
focused on answering: "What requires my attention right now?"
## Backend Implementation
### Dashboard Service (NEW)
- Health status calculation (green/yellow/red traffic light)
- Action queue prioritization (critical/important/normal)
- Orchestration summary with narrative format
- Production timeline transformation
- Insights calculation and consequence prediction
### API Endpoints (NEW)
- GET /dashboard/health-status - Overall bakery health indicator
- GET /dashboard/orchestration-summary - What system did automatically
- GET /dashboard/action-queue - Prioritized tasks requiring attention
- GET /dashboard/production-timeline - Today's production schedule
- GET /dashboard/insights - Key metrics (savings, inventory, waste, deliveries)
### Enhanced Models
- PurchaseOrder: Added reasoning, consequence, reasoning_data fields
- ProductionBatch: Added reasoning, reasoning_data fields
- Enables transparency into automation decisions
## Frontend Implementation
### API Hooks (NEW)
- useBakeryHealthStatus() - Real-time health monitoring
- useOrchestrationSummary() - System transparency
- useActionQueue() - Prioritized action management
- useProductionTimeline() - Production tracking
- useInsights() - Glanceable metrics
### Dashboard Components (NEW)
- HealthStatusCard: Traffic light indicator with checklist
- ActionQueueCard: Prioritized actions with reasoning/consequences
- OrchestrationSummaryCard: Narrative of what system did
- ProductionTimelineCard: Chronological production view
- InsightsGrid: 2x2 grid of key metrics
### Main Dashboard Page (REPLACED)
- Complete rewrite with mobile-first design
- All sections integrated with error handling
- Real-time refresh and quick action links
- Old dashboard backed up as DashboardPage.legacy.tsx
## Key Features
### Automation-First
- Shows what orchestrator did overnight
- Builds trust through transparency
- Explains reasoning for all automated decisions
### Action-Oriented
- Prioritizes tasks over information display
- Clear consequences for each action
- Large touch-friendly buttons
### Progressive Disclosure
- Shows 20% of info that matters 80% of time
- Expandable details when needed
- No overwhelming metrics
### Mobile-First
- One-handed operation
- Large touch targets (min 44px)
- Responsive grid layouts
### Trust-Building
- Narrative format ("I planned your day")
- Reasoning inputs transparency
- Clear status indicators
## User Segments Supported
1. Solo Bakery Owner (Primary)
- Simple health indicator
- Action checklist (max 3-5 items)
- Mobile-optimized
2. Multi-Location Owner
- Multi-tenant support (existing)
- Comparison capabilities
- Delegation ready
3. Enterprise/Central Bakery (Future)
- Network topology support
- Advanced analytics ready
## JTBD Analysis Delivered
Main Job: "Help me quickly understand bakery status and know what needs my intervention"
Emotional Jobs Addressed:
- Feel in control despite automation
- Reduce daily anxiety
- Feel competent with technology
- Trust system as safety net
Social Jobs Addressed:
- Demonstrate professional management
- Avoid being bottleneck
- Show sustainability
## Technical Stack
Backend: Python, FastAPI, SQLAlchemy, PostgreSQL
Frontend: React, TypeScript, TanStack Query, Tailwind CSS
Architecture: Microservices with circuit breakers
## Breaking Changes
- Complete dashboard page rewrite (old version backed up)
- New API endpoints require orchestrator service deployment
- Database migrations needed for reasoning fields
## Migration Required
Run migrations to add new model fields:
- purchase_orders: reasoning, consequence, reasoning_data
- production_batches: reasoning, reasoning_data
## Documentation
See DASHBOARD_REDESIGN_SUMMARY.md for complete implementation details,
JTBD analysis, success metrics, and deployment guide.
BREAKING CHANGE: Dashboard page completely redesigned with new data structures
2025-11-07 17:10:17 +00:00
|
|
|
# }
|
|
|
|
|
|
2025-10-30 21:08:07 +01:00
|
|
|
# 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'),
|
|
|
|
|
)
|