Files

733 lines
24 KiB
Python
Raw Permalink Normal View History

# services/suppliers/app/schemas/suppliers.py
"""
Pydantic schemas for supplier-related API requests and responses
"""
from pydantic import BaseModel, Field, EmailStr
2025-10-30 21:08:07 +01:00
from typing import List, Optional, Dict, Any, Union
from uuid import UUID
from datetime import datetime
from decimal import Decimal
from app.models.suppliers import (
2025-10-30 21:08:07 +01:00
SupplierType, SupplierStatus, PaymentTerms,
QualityRating
)
2025-10-30 21:08:07 +01:00
# NOTE: PO, Delivery, and Invoice schemas remain for backward compatibility
2025-12-05 20:07:01 +01:00
# The primary implementation has moved to Procurement Service (services/procurement/)
# These schemas support legacy endpoints in suppliers service (app/api/purchase_orders.py)
#
# Migration Status:
# - ✅ Procurement Service fully operational with enhanced features
# - ⚠️ Supplier service endpoints still active for backward compatibility
# - 📋 Deprecation Timeline: Q2 2026 (after 6-month dual-operation period)
#
# Action Required:
# 1. All new integrations should use Procurement Service endpoints
# 2. Update client applications to use ProcurementServiceClient
# 3. Monitor usage of supplier service PO endpoints via logs
# 4. Plan migration of remaining clients by Q1 2026
2025-10-30 21:08:07 +01:00
# ============================================================================
# SUPPLIER SCHEMAS
# ============================================================================
class SupplierCreate(BaseModel):
"""Schema for creating suppliers"""
name: str = Field(..., min_length=1, max_length=255)
supplier_code: Optional[str] = Field(None, max_length=50)
tax_id: Optional[str] = Field(None, max_length=50)
registration_number: Optional[str] = Field(None, max_length=100)
supplier_type: SupplierType
contact_person: Optional[str] = Field(None, max_length=200)
email: Optional[EmailStr] = None
phone: Optional[str] = Field(None, max_length=30)
mobile: Optional[str] = Field(None, max_length=30)
website: Optional[str] = Field(None, max_length=255)
# Address
address_line1: Optional[str] = Field(None, max_length=255)
address_line2: Optional[str] = Field(None, max_length=255)
city: Optional[str] = Field(None, max_length=100)
state_province: Optional[str] = Field(None, max_length=100)
postal_code: Optional[str] = Field(None, max_length=20)
country: Optional[str] = Field(None, max_length=100)
# Business terms
payment_terms: PaymentTerms = PaymentTerms.net_30
credit_limit: Optional[Decimal] = Field(None, ge=0)
currency: str = Field(default="EUR", max_length=3)
standard_lead_time: int = Field(default=3, ge=0, le=365)
minimum_order_amount: Optional[Decimal] = Field(None, ge=0)
delivery_area: Optional[str] = Field(None, max_length=255)
# Additional information
notes: Optional[str] = None
2025-10-30 21:08:07 +01:00
certifications: Optional[Union[Dict[str, Any], List[str]]] = None
business_hours: Optional[Dict[str, Any]] = None
specializations: Optional[Dict[str, Any]] = None
class SupplierUpdate(BaseModel):
"""Schema for updating suppliers"""
name: Optional[str] = Field(None, min_length=1, max_length=255)
supplier_code: Optional[str] = Field(None, max_length=50)
tax_id: Optional[str] = Field(None, max_length=50)
registration_number: Optional[str] = Field(None, max_length=100)
supplier_type: Optional[SupplierType] = None
status: Optional[SupplierStatus] = None
contact_person: Optional[str] = Field(None, max_length=200)
email: Optional[EmailStr] = None
phone: Optional[str] = Field(None, max_length=30)
mobile: Optional[str] = Field(None, max_length=30)
website: Optional[str] = Field(None, max_length=255)
# Address
address_line1: Optional[str] = Field(None, max_length=255)
address_line2: Optional[str] = Field(None, max_length=255)
city: Optional[str] = Field(None, max_length=100)
state_province: Optional[str] = Field(None, max_length=100)
postal_code: Optional[str] = Field(None, max_length=20)
country: Optional[str] = Field(None, max_length=100)
# Business terms
payment_terms: Optional[PaymentTerms] = None
credit_limit: Optional[Decimal] = Field(None, ge=0)
currency: Optional[str] = Field(None, max_length=3)
standard_lead_time: Optional[int] = Field(None, ge=0, le=365)
minimum_order_amount: Optional[Decimal] = Field(None, ge=0)
delivery_area: Optional[str] = Field(None, max_length=255)
# Additional information
notes: Optional[str] = None
2025-10-30 21:08:07 +01:00
certifications: Optional[Union[Dict[str, Any], List[str]]] = None
business_hours: Optional[Dict[str, Any]] = None
specializations: Optional[Dict[str, Any]] = None
class SupplierApproval(BaseModel):
"""Schema for supplier approval/rejection"""
2025-08-15 22:40:19 +02:00
action: str = Field(..., pattern="^(approve|reject)$")
notes: Optional[str] = None
class SupplierResponse(BaseModel):
"""Schema for supplier responses"""
id: UUID
tenant_id: UUID
name: str
supplier_code: Optional[str] = None
tax_id: Optional[str] = None
registration_number: Optional[str] = None
supplier_type: SupplierType
status: SupplierStatus
contact_person: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
mobile: Optional[str] = None
website: Optional[str] = None
# Address
address_line1: Optional[str] = None
address_line2: Optional[str] = None
city: Optional[str] = None
state_province: Optional[str] = None
postal_code: Optional[str] = None
country: Optional[str] = None
# Business terms
payment_terms: PaymentTerms
credit_limit: Optional[Decimal] = None
currency: str
standard_lead_time: int
minimum_order_amount: Optional[Decimal] = None
delivery_area: Optional[str] = None
# Performance metrics
quality_rating: Optional[float] = None
delivery_rating: Optional[float] = None
total_orders: int
total_amount: Decimal
# Approval info
approved_by: Optional[UUID] = None
approved_at: Optional[datetime] = None
rejection_reason: Optional[str] = None
# Additional information
notes: Optional[str] = None
2025-10-30 21:08:07 +01:00
certifications: Optional[Union[Dict[str, Any], List[str]]] = None
business_hours: Optional[Dict[str, Any]] = None
specializations: Optional[Dict[str, Any]] = None
# Audit fields
created_at: datetime
updated_at: datetime
created_by: UUID
updated_by: UUID
class Config:
from_attributes = True
class SupplierSummary(BaseModel):
"""Schema for supplier summary (list view)"""
id: UUID
name: str
supplier_code: Optional[str] = None
supplier_type: SupplierType
status: SupplierStatus
contact_person: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
city: Optional[str] = None
country: Optional[str] = None
2025-10-27 16:33:26 +01:00
# Business terms - Added for list view
payment_terms: PaymentTerms
standard_lead_time: int
minimum_order_amount: Optional[Decimal] = None
# Performance metrics
quality_rating: Optional[float] = None
delivery_rating: Optional[float] = None
total_orders: int
total_amount: Decimal
created_at: datetime
class Config:
from_attributes = True
2025-10-27 16:33:26 +01:00
class SupplierDeletionSummary(BaseModel):
"""Schema for supplier deletion summary"""
supplier_name: str
deleted_price_lists: int = 0
deleted_quality_reviews: int = 0
deleted_performance_metrics: int = 0
deleted_alerts: int = 0
deleted_scorecards: int = 0
deletion_timestamp: datetime
class Config:
from_attributes = True
# ============================================================================
# PURCHASE ORDER SCHEMAS
# ============================================================================
class PurchaseOrderItemCreate(BaseModel):
"""Schema for creating purchase order items"""
2025-08-14 16:47:34 +02:00
inventory_product_id: UUID
product_code: Optional[str] = Field(None, max_length=100)
ordered_quantity: int = Field(..., gt=0)
unit_of_measure: str = Field(..., max_length=20)
unit_price: Decimal = Field(..., gt=0)
quality_requirements: Optional[str] = None
item_notes: Optional[str] = None
class PurchaseOrderItemUpdate(BaseModel):
"""Schema for updating purchase order items"""
ordered_quantity: Optional[int] = Field(None, gt=0)
unit_price: Optional[Decimal] = Field(None, gt=0)
quality_requirements: Optional[str] = None
item_notes: Optional[str] = None
class PurchaseOrderItemResponse(BaseModel):
"""Schema for purchase order item responses"""
id: UUID
tenant_id: UUID
purchase_order_id: UUID
price_list_item_id: Optional[UUID] = None
2025-08-14 16:47:34 +02:00
inventory_product_id: UUID
product_code: Optional[str] = None
ordered_quantity: int
unit_of_measure: str
unit_price: Decimal
line_total: Decimal
received_quantity: int
remaining_quantity: int
quality_requirements: Optional[str] = None
item_notes: Optional[str] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class PurchaseOrderCreate(BaseModel):
"""Schema for creating purchase orders"""
supplier_id: UUID
reference_number: Optional[str] = Field(None, max_length=100)
priority: str = Field(default="normal", max_length=20)
required_delivery_date: Optional[datetime] = None
# Delivery information
delivery_address: Optional[str] = None
delivery_instructions: Optional[str] = None
delivery_contact: Optional[str] = Field(None, max_length=200)
delivery_phone: Optional[str] = Field(None, max_length=30)
# Financial information
tax_amount: Decimal = Field(default=0, ge=0)
shipping_cost: Decimal = Field(default=0, ge=0)
discount_amount: Decimal = Field(default=0, ge=0)
# Additional information
notes: Optional[str] = None
internal_notes: Optional[str] = None
terms_and_conditions: Optional[str] = None
# Items
items: List[PurchaseOrderItemCreate] = Field(..., min_items=1)
class PurchaseOrderUpdate(BaseModel):
"""Schema for updating purchase orders"""
reference_number: Optional[str] = Field(None, max_length=100)
priority: Optional[str] = Field(None, max_length=20)
required_delivery_date: Optional[datetime] = None
estimated_delivery_date: Optional[datetime] = None
# Delivery information
delivery_address: Optional[str] = None
delivery_instructions: Optional[str] = None
delivery_contact: Optional[str] = Field(None, max_length=200)
delivery_phone: Optional[str] = Field(None, max_length=30)
# Financial information
tax_amount: Optional[Decimal] = Field(None, ge=0)
shipping_cost: Optional[Decimal] = Field(None, ge=0)
discount_amount: Optional[Decimal] = Field(None, ge=0)
# Additional information
notes: Optional[str] = None
internal_notes: Optional[str] = None
terms_and_conditions: Optional[str] = None
# Supplier communication
supplier_reference: Optional[str] = Field(None, max_length=100)
class PurchaseOrderStatusUpdate(BaseModel):
"""Schema for updating purchase order status"""
2025-10-30 21:08:07 +01:00
status: str # PurchaseOrderStatus - moved to Procurement Service
notes: Optional[str] = None
class PurchaseOrderApproval(BaseModel):
"""Schema for purchase order approval/rejection"""
2025-08-15 22:40:19 +02:00
action: str = Field(..., pattern="^(approve|reject)$")
notes: Optional[str] = None
class PurchaseOrderResponse(BaseModel):
"""Schema for purchase order responses"""
id: UUID
tenant_id: UUID
supplier_id: UUID
po_number: str
reference_number: Optional[str] = None
2025-10-30 21:08:07 +01:00
status: str # PurchaseOrderStatus
priority: str
order_date: datetime
required_delivery_date: Optional[datetime] = None
estimated_delivery_date: Optional[datetime] = None
# Financial information
subtotal: Decimal
tax_amount: Decimal
shipping_cost: Decimal
discount_amount: Decimal
total_amount: Decimal
currency: str
# Delivery information
delivery_address: Optional[str] = None
delivery_instructions: Optional[str] = None
delivery_contact: Optional[str] = None
delivery_phone: Optional[str] = None
# Approval workflow
requires_approval: bool
approved_by: Optional[UUID] = None
approved_at: Optional[datetime] = None
rejection_reason: Optional[str] = None
# Communication tracking
sent_to_supplier_at: Optional[datetime] = None
supplier_confirmation_date: Optional[datetime] = None
supplier_reference: Optional[str] = None
# Additional information
notes: Optional[str] = None
internal_notes: Optional[str] = None
terms_and_conditions: Optional[str] = None
# Audit fields
created_at: datetime
updated_at: datetime
created_by: UUID
updated_by: UUID
# Related data (populated separately)
supplier: Optional[SupplierSummary] = None
items: Optional[List[PurchaseOrderItemResponse]] = None
class Config:
from_attributes = True
class PurchaseOrderSummary(BaseModel):
"""Schema for purchase order summary (list view)"""
id: UUID
po_number: str
supplier_id: UUID
supplier_name: Optional[str] = None
2025-10-30 21:08:07 +01:00
status: str # PurchaseOrderStatus
priority: str
order_date: datetime
required_delivery_date: Optional[datetime] = None
total_amount: Decimal
currency: str
created_at: datetime
class Config:
from_attributes = True
# ============================================================================
# DELIVERY SCHEMAS
# ============================================================================
class DeliveryItemCreate(BaseModel):
"""Schema for creating delivery items"""
purchase_order_item_id: UUID
2025-08-14 16:47:34 +02:00
inventory_product_id: UUID
ordered_quantity: int = Field(..., gt=0)
delivered_quantity: int = Field(..., ge=0)
accepted_quantity: int = Field(..., ge=0)
rejected_quantity: int = Field(default=0, ge=0)
# Quality information
batch_lot_number: Optional[str] = Field(None, max_length=100)
expiry_date: Optional[datetime] = None
quality_grade: Optional[str] = Field(None, max_length=20)
# Issues and notes
quality_issues: Optional[str] = None
rejection_reason: Optional[str] = None
item_notes: Optional[str] = None
class DeliveryItemResponse(BaseModel):
"""Schema for delivery item responses"""
id: UUID
tenant_id: UUID
delivery_id: UUID
purchase_order_item_id: UUID
2025-08-14 16:47:34 +02:00
inventory_product_id: UUID
ordered_quantity: int
delivered_quantity: int
accepted_quantity: int
rejected_quantity: int
batch_lot_number: Optional[str] = None
expiry_date: Optional[datetime] = None
quality_grade: Optional[str] = None
quality_issues: Optional[str] = None
rejection_reason: Optional[str] = None
item_notes: Optional[str] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class DeliveryCreate(BaseModel):
"""Schema for creating deliveries"""
purchase_order_id: UUID
supplier_id: UUID
supplier_delivery_note: Optional[str] = Field(None, max_length=100)
scheduled_date: Optional[datetime] = None
estimated_arrival: Optional[datetime] = None
# Delivery details
delivery_address: Optional[str] = None
delivery_contact: Optional[str] = Field(None, max_length=200)
delivery_phone: Optional[str] = Field(None, max_length=30)
carrier_name: Optional[str] = Field(None, max_length=200)
tracking_number: Optional[str] = Field(None, max_length=100)
# Additional information
notes: Optional[str] = None
# Items
items: List[DeliveryItemCreate] = Field(..., min_items=1)
class DeliveryUpdate(BaseModel):
"""Schema for updating deliveries"""
supplier_delivery_note: Optional[str] = Field(None, max_length=100)
scheduled_date: Optional[datetime] = None
estimated_arrival: Optional[datetime] = None
actual_arrival: Optional[datetime] = None
# Delivery details
delivery_address: Optional[str] = None
delivery_contact: Optional[str] = Field(None, max_length=200)
delivery_phone: Optional[str] = Field(None, max_length=30)
carrier_name: Optional[str] = Field(None, max_length=200)
tracking_number: Optional[str] = Field(None, max_length=100)
# Quality inspection
inspection_passed: Optional[bool] = None
inspection_notes: Optional[str] = None
quality_issues: Optional[Dict[str, Any]] = None
# Additional information
notes: Optional[str] = None
class DeliveryStatusUpdate(BaseModel):
"""Schema for updating delivery status"""
2025-10-30 21:08:07 +01:00
status: str # DeliveryStatus
notes: Optional[str] = None
update_timestamps: bool = Field(default=True)
class DeliveryReceiptConfirmation(BaseModel):
"""Schema for confirming delivery receipt"""
inspection_passed: bool = True
inspection_notes: Optional[str] = None
quality_issues: Optional[Dict[str, Any]] = None
notes: Optional[str] = None
class DeliveryResponse(BaseModel):
"""Schema for delivery responses"""
id: UUID
tenant_id: UUID
purchase_order_id: UUID
supplier_id: UUID
delivery_number: str
supplier_delivery_note: Optional[str] = None
2025-10-30 21:08:07 +01:00
status: str # DeliveryStatus
# Timing
scheduled_date: Optional[datetime] = None
estimated_arrival: Optional[datetime] = None
actual_arrival: Optional[datetime] = None
completed_at: Optional[datetime] = None
# Delivery details
delivery_address: Optional[str] = None
delivery_contact: Optional[str] = None
delivery_phone: Optional[str] = None
carrier_name: Optional[str] = None
tracking_number: Optional[str] = None
# Quality inspection
inspection_passed: Optional[bool] = None
inspection_notes: Optional[str] = None
quality_issues: Optional[Dict[str, Any]] = None
# Receipt information
received_by: Optional[UUID] = None
received_at: Optional[datetime] = None
# Additional information
notes: Optional[str] = None
photos: Optional[Dict[str, Any]] = None
# Audit fields
created_at: datetime
updated_at: datetime
created_by: UUID
# Related data
supplier: Optional[SupplierSummary] = None
purchase_order: Optional[PurchaseOrderSummary] = None
items: Optional[List[DeliveryItemResponse]] = None
class Config:
from_attributes = True
class DeliverySummary(BaseModel):
"""Schema for delivery summary (list view)"""
id: UUID
delivery_number: str
supplier_id: UUID
supplier_name: Optional[str] = None
purchase_order_id: UUID
po_number: Optional[str] = None
2025-10-30 21:08:07 +01:00
status: str # DeliveryStatus
scheduled_date: Optional[datetime] = None
actual_arrival: Optional[datetime] = None
inspection_passed: Optional[bool] = None
created_at: datetime
class Config:
from_attributes = True
# ============================================================================
# SEARCH AND FILTER SCHEMAS
# ============================================================================
class SupplierSearchParams(BaseModel):
"""Search parameters for suppliers"""
search_term: Optional[str] = Field(None, max_length=100)
supplier_type: Optional[SupplierType] = None
status: Optional[SupplierStatus] = None
limit: int = Field(default=50, ge=1, le=1000)
offset: int = Field(default=0, ge=0)
class PurchaseOrderSearchParams(BaseModel):
"""Search parameters for purchase orders"""
supplier_id: Optional[UUID] = None
2025-10-30 21:08:07 +01:00
status: Optional[str] = None # PurchaseOrderStatus
priority: Optional[str] = None
date_from: Optional[datetime] = None
date_to: Optional[datetime] = None
search_term: Optional[str] = Field(None, max_length=100)
limit: int = Field(default=50, ge=1, le=1000)
offset: int = Field(default=0, ge=0)
class DeliverySearchParams(BaseModel):
"""Search parameters for deliveries"""
supplier_id: Optional[UUID] = None
2025-10-30 21:08:07 +01:00
status: Optional[str] = None # DeliveryStatus
date_from: Optional[datetime] = None
date_to: Optional[datetime] = None
search_term: Optional[str] = Field(None, max_length=100)
limit: int = Field(default=50, ge=1, le=1000)
offset: int = Field(default=0, ge=0)
2025-10-29 06:58:05 +01:00
# ============================================================================
# SUPPLIER PRICE LIST SCHEMAS
# ============================================================================
class SupplierPriceListCreate(BaseModel):
"""Schema for creating supplier price list items"""
inventory_product_id: UUID
product_code: Optional[str] = Field(None, max_length=100)
unit_price: Decimal = Field(..., gt=0)
unit_of_measure: str = Field(..., max_length=20)
minimum_order_quantity: Optional[int] = Field(None, ge=1)
price_per_unit: Decimal = Field(..., gt=0)
tier_pricing: Optional[Dict[str, Any]] = None # [{quantity: 100, price: 2.50}, ...]
effective_date: Optional[datetime] = Field(default_factory=lambda: datetime.now())
expiry_date: Optional[datetime] = None
is_active: bool = True
brand: Optional[str] = Field(None, max_length=100)
packaging_size: Optional[str] = Field(None, max_length=50)
origin_country: Optional[str] = Field(None, max_length=100)
shelf_life_days: Optional[int] = None
storage_requirements: Optional[str] = None
quality_specs: Optional[Dict[str, Any]] = None
allergens: Optional[Dict[str, Any]] = None
class SupplierPriceListUpdate(BaseModel):
"""Schema for updating supplier price list items"""
unit_price: Optional[Decimal] = Field(None, gt=0)
unit_of_measure: Optional[str] = Field(None, max_length=20)
minimum_order_quantity: Optional[int] = Field(None, ge=1)
tier_pricing: Optional[Dict[str, Any]] = None
effective_date: Optional[datetime] = None
expiry_date: Optional[datetime] = None
is_active: Optional[bool] = None
brand: Optional[str] = Field(None, max_length=100)
packaging_size: Optional[str] = Field(None, max_length=50)
origin_country: Optional[str] = Field(None, max_length=100)
shelf_life_days: Optional[int] = None
storage_requirements: Optional[str] = None
quality_specs: Optional[Dict[str, Any]] = None
allergens: Optional[Dict[str, Any]] = None
class SupplierPriceListResponse(BaseModel):
"""Schema for supplier price list responses"""
id: UUID
tenant_id: UUID
supplier_id: UUID
inventory_product_id: UUID
product_code: Optional[str] = None
unit_price: Decimal
unit_of_measure: str
minimum_order_quantity: Optional[int] = None
price_per_unit: Decimal
tier_pricing: Optional[Dict[str, Any]] = None
effective_date: datetime
expiry_date: Optional[datetime] = None
is_active: bool
brand: Optional[str] = None
packaging_size: Optional[str] = None
origin_country: Optional[str] = None
shelf_life_days: Optional[int] = None
storage_requirements: Optional[str] = None
quality_specs: Optional[Dict[str, Any]] = None
allergens: Optional[Dict[str, Any]] = None
created_at: datetime
updated_at: datetime
created_by: UUID
updated_by: UUID
class Config:
from_attributes = True
# ============================================================================
# STATISTICS AND REPORTING SCHEMAS
# ============================================================================
class SupplierStatistics(BaseModel):
"""Schema for supplier statistics"""
total_suppliers: int
active_suppliers: int
pending_suppliers: int
avg_quality_rating: float
avg_delivery_rating: float
total_spend: float
class PurchaseOrderStatistics(BaseModel):
"""Schema for purchase order statistics"""
total_orders: int
status_counts: Dict[str, int]
this_month_orders: int
this_month_spend: float
avg_order_value: float
overdue_count: int
pending_approval: int
class DeliveryPerformanceStats(BaseModel):
"""Schema for delivery performance statistics"""
total_deliveries: int
on_time_deliveries: int
late_deliveries: int
failed_deliveries: int
on_time_percentage: float
avg_delay_hours: float
quality_pass_rate: float
class DeliverySummaryStats(BaseModel):
"""Schema for delivery summary statistics"""
todays_deliveries: int
this_week_deliveries: int
overdue_deliveries: int
2025-10-29 06:58:05 +01:00
in_transit_deliveries: int