Files
bakery-ia/services/inventory/app/schemas/inventory.py
2025-09-18 23:32:53 +02:00

485 lines
20 KiB
Python

# services/inventory/app/schemas/inventory.py
"""
Pydantic schemas for inventory API requests and responses
"""
from typing import Optional, List, Dict, Any
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel, Field, validator
from typing import Generic, TypeVar
from enum import Enum
from app.models.inventory import UnitOfMeasure, IngredientCategory, StockMovementType, ProductType, ProductCategory, ProductionStage
T = TypeVar('T')
# ===== BASE SCHEMAS =====
class InventoryBaseSchema(BaseModel):
"""Base schema for inventory models"""
class Config:
from_attributes = True
use_enum_values = True
json_encoders = {
datetime: lambda v: v.isoformat() if v else None,
Decimal: lambda v: float(v) if v else None
}
# ===== INGREDIENT SCHEMAS =====
class IngredientCreate(InventoryBaseSchema):
"""Schema for creating ingredients and finished products"""
name: str = Field(..., max_length=255, description="Product name")
product_type: ProductType = Field(ProductType.INGREDIENT, description="Type of product (ingredient or finished_product)")
sku: Optional[str] = Field(None, max_length=100, description="SKU code")
barcode: Optional[str] = Field(None, max_length=50, description="Barcode")
category: Optional[str] = Field(None, description="Product category (ingredient or finished product category)")
subcategory: Optional[str] = Field(None, max_length=100, description="Subcategory")
description: Optional[str] = Field(None, description="Ingredient description")
brand: Optional[str] = Field(None, max_length=100, description="Brand name")
unit_of_measure: UnitOfMeasure = Field(..., description="Unit of measure")
package_size: Optional[float] = Field(None, gt=0, description="Package size")
# Pricing
average_cost: Optional[Decimal] = Field(None, ge=0, description="Average cost per unit")
standard_cost: Optional[Decimal] = Field(None, ge=0, description="Standard cost per unit")
# Stock management
low_stock_threshold: float = Field(10.0, ge=0, description="Low stock alert threshold")
reorder_point: float = Field(20.0, ge=0, description="Reorder point")
reorder_quantity: float = Field(50.0, gt=0, description="Default reorder quantity")
max_stock_level: Optional[float] = Field(None, gt=0, description="Maximum stock level")
# Shelf life (default value only - actual per batch)
shelf_life_days: Optional[int] = Field(None, gt=0, description="Default shelf life in days")
# Properties
is_perishable: bool = Field(False, description="Is perishable")
allergen_info: Optional[Dict[str, Any]] = Field(None, description="Allergen information")
@validator('reorder_point')
def validate_reorder_point(cls, v, values):
if 'low_stock_threshold' in values and v <= values['low_stock_threshold']:
raise ValueError('Reorder point must be greater than low stock threshold')
return v
class IngredientUpdate(InventoryBaseSchema):
"""Schema for updating ingredients and finished products"""
name: Optional[str] = Field(None, max_length=255, description="Product name")
product_type: Optional[ProductType] = Field(None, description="Type of product (ingredient or finished_product)")
sku: Optional[str] = Field(None, max_length=100, description="SKU code")
barcode: Optional[str] = Field(None, max_length=50, description="Barcode")
category: Optional[str] = Field(None, description="Product category")
subcategory: Optional[str] = Field(None, max_length=100, description="Subcategory")
description: Optional[str] = Field(None, description="Ingredient description")
brand: Optional[str] = Field(None, max_length=100, description="Brand name")
unit_of_measure: Optional[UnitOfMeasure] = Field(None, description="Unit of measure")
package_size: Optional[float] = Field(None, gt=0, description="Package size")
# Pricing
average_cost: Optional[Decimal] = Field(None, ge=0, description="Average cost per unit")
standard_cost: Optional[Decimal] = Field(None, ge=0, description="Standard cost per unit")
# Stock management
low_stock_threshold: Optional[float] = Field(None, ge=0, description="Low stock alert threshold")
reorder_point: Optional[float] = Field(None, ge=0, description="Reorder point")
reorder_quantity: Optional[float] = Field(None, gt=0, description="Default reorder quantity")
max_stock_level: Optional[float] = Field(None, gt=0, description="Maximum stock level")
# Shelf life (default value only - actual per batch)
shelf_life_days: Optional[int] = Field(None, gt=0, description="Default shelf life in days")
# Properties
is_active: Optional[bool] = Field(None, description="Is active")
is_perishable: Optional[bool] = Field(None, description="Is perishable")
allergen_info: Optional[Dict[str, Any]] = Field(None, description="Allergen information")
class IngredientResponse(InventoryBaseSchema):
"""Schema for ingredient and finished product API responses"""
id: str
tenant_id: str
name: str
product_type: ProductType
sku: Optional[str]
barcode: Optional[str]
category: Optional[str] # Will be populated from ingredient_category or product_category
subcategory: Optional[str]
description: Optional[str]
brand: Optional[str]
unit_of_measure: UnitOfMeasure
package_size: Optional[float]
average_cost: Optional[float]
last_purchase_price: Optional[float]
standard_cost: Optional[float]
low_stock_threshold: float
reorder_point: float
reorder_quantity: float
max_stock_level: Optional[float]
shelf_life_days: Optional[int] # Default value only
is_active: bool
is_perishable: bool
allergen_info: Optional[Dict[str, Any]]
created_at: datetime
updated_at: datetime
created_by: Optional[str]
# Computed fields
current_stock: Optional[float] = None
is_low_stock: Optional[bool] = None
needs_reorder: Optional[bool] = None
# ===== STOCK SCHEMAS =====
class StockCreate(InventoryBaseSchema):
"""Schema for creating stock entries"""
ingredient_id: str = Field(..., description="Ingredient ID")
supplier_id: Optional[str] = Field(None, description="Supplier ID")
batch_number: Optional[str] = Field(None, max_length=100, description="Batch number")
lot_number: Optional[str] = Field(None, max_length=100, description="Lot number")
supplier_batch_ref: Optional[str] = Field(None, max_length=100, description="Supplier batch reference")
# Production stage tracking
production_stage: ProductionStage = Field(default=ProductionStage.RAW_INGREDIENT, description="Production stage of the stock")
transformation_reference: Optional[str] = Field(None, max_length=100, description="Transformation reference ID")
current_quantity: float = Field(..., ge=0, description="Current quantity")
received_date: Optional[datetime] = Field(None, description="Date received")
expiration_date: Optional[datetime] = Field(None, description="Expiration date")
best_before_date: Optional[datetime] = Field(None, description="Best before date")
# Stage-specific expiration fields
original_expiration_date: Optional[datetime] = Field(None, description="Original batch expiration (for par-baked items)")
transformation_date: Optional[datetime] = Field(None, description="Date when product was transformed")
final_expiration_date: Optional[datetime] = Field(None, description="Final expiration after transformation")
unit_cost: Optional[Decimal] = Field(None, ge=0, description="Unit cost")
storage_location: Optional[str] = Field(None, max_length=100, description="Storage location")
warehouse_zone: Optional[str] = Field(None, max_length=50, description="Warehouse zone")
shelf_position: Optional[str] = Field(None, max_length=50, description="Shelf position")
quality_status: str = Field("good", description="Quality status")
# Batch-specific storage requirements
requires_refrigeration: bool = Field(False, description="Requires refrigeration")
requires_freezing: bool = Field(False, description="Requires freezing")
storage_temperature_min: Optional[float] = Field(None, description="Min storage temperature (°C)")
storage_temperature_max: Optional[float] = Field(None, description="Max storage temperature (°C)")
storage_humidity_max: Optional[float] = Field(None, ge=0, le=100, description="Max humidity (%)")
shelf_life_days: Optional[int] = Field(None, gt=0, description="Batch-specific shelf life in days")
storage_instructions: Optional[str] = Field(None, description="Batch-specific storage instructions")
@validator('storage_temperature_max')
def validate_temperature_range(cls, v, values):
min_temp = values.get('storage_temperature_min')
if v is not None and min_temp is not None and v <= min_temp:
raise ValueError('Max temperature must be greater than min temperature')
return v
class StockUpdate(InventoryBaseSchema):
"""Schema for updating stock entries"""
supplier_id: Optional[str] = Field(None, description="Supplier ID")
batch_number: Optional[str] = Field(None, max_length=100, description="Batch number")
lot_number: Optional[str] = Field(None, max_length=100, description="Lot number")
supplier_batch_ref: Optional[str] = Field(None, max_length=100, description="Supplier batch reference")
# Production stage tracking
production_stage: Optional[ProductionStage] = Field(None, description="Production stage of the stock")
transformation_reference: Optional[str] = Field(None, max_length=100, description="Transformation reference ID")
current_quantity: Optional[float] = Field(None, ge=0, description="Current quantity")
reserved_quantity: Optional[float] = Field(None, ge=0, description="Reserved quantity")
received_date: Optional[datetime] = Field(None, description="Date received")
expiration_date: Optional[datetime] = Field(None, description="Expiration date")
best_before_date: Optional[datetime] = Field(None, description="Best before date")
# Stage-specific expiration fields
original_expiration_date: Optional[datetime] = Field(None, description="Original batch expiration (for par-baked items)")
transformation_date: Optional[datetime] = Field(None, description="Date when product was transformed")
final_expiration_date: Optional[datetime] = Field(None, description="Final expiration after transformation")
unit_cost: Optional[Decimal] = Field(None, ge=0, description="Unit cost")
storage_location: Optional[str] = Field(None, max_length=100, description="Storage location")
warehouse_zone: Optional[str] = Field(None, max_length=50, description="Warehouse zone")
shelf_position: Optional[str] = Field(None, max_length=50, description="Shelf position")
is_available: Optional[bool] = Field(None, description="Is available")
quality_status: Optional[str] = Field(None, description="Quality status")
# Batch-specific storage requirements
requires_refrigeration: Optional[bool] = Field(None, description="Requires refrigeration")
requires_freezing: Optional[bool] = Field(None, description="Requires freezing")
storage_temperature_min: Optional[float] = Field(None, description="Min storage temperature (°C)")
storage_temperature_max: Optional[float] = Field(None, description="Max storage temperature (°C)")
storage_humidity_max: Optional[float] = Field(None, ge=0, le=100, description="Max humidity (%)")
shelf_life_days: Optional[int] = Field(None, gt=0, description="Batch-specific shelf life in days")
storage_instructions: Optional[str] = Field(None, description="Batch-specific storage instructions")
class StockResponse(InventoryBaseSchema):
"""Schema for stock API responses"""
id: str
tenant_id: str
ingredient_id: str
supplier_id: Optional[str]
batch_number: Optional[str]
lot_number: Optional[str]
supplier_batch_ref: Optional[str]
# Production stage tracking
production_stage: ProductionStage
transformation_reference: Optional[str]
current_quantity: float
reserved_quantity: float
available_quantity: float
received_date: Optional[datetime]
expiration_date: Optional[datetime]
best_before_date: Optional[datetime]
# Stage-specific expiration fields
original_expiration_date: Optional[datetime]
transformation_date: Optional[datetime]
final_expiration_date: Optional[datetime]
unit_cost: Optional[float]
total_cost: Optional[float]
storage_location: Optional[str]
warehouse_zone: Optional[str]
shelf_position: Optional[str]
is_available: bool
is_expired: bool
quality_status: str
# Batch-specific storage requirements
requires_refrigeration: bool
requires_freezing: bool
storage_temperature_min: Optional[float]
storage_temperature_max: Optional[float]
storage_humidity_max: Optional[float]
shelf_life_days: Optional[int]
storage_instructions: Optional[str]
created_at: datetime
updated_at: datetime
# Related data
ingredient: Optional[IngredientResponse] = None
# ===== STOCK MOVEMENT SCHEMAS =====
class StockMovementCreate(InventoryBaseSchema):
"""Schema for creating stock movements"""
ingredient_id: str = Field(..., description="Ingredient ID")
stock_id: Optional[str] = Field(None, description="Stock ID")
movement_type: StockMovementType = Field(..., description="Movement type")
quantity: float = Field(..., description="Quantity moved")
unit_cost: Optional[Decimal] = Field(None, ge=0, description="Unit cost")
reference_number: Optional[str] = Field(None, max_length=100, description="Reference number")
supplier_id: Optional[str] = Field(None, description="Supplier ID")
notes: Optional[str] = Field(None, description="Movement notes")
reason_code: Optional[str] = Field(None, max_length=50, description="Reason code")
movement_date: Optional[datetime] = Field(None, description="Movement date")
class StockMovementResponse(InventoryBaseSchema):
"""Schema for stock movement API responses"""
id: str
tenant_id: str
ingredient_id: str
stock_id: Optional[str]
movement_type: StockMovementType
quantity: float
unit_cost: Optional[float]
total_cost: Optional[float]
quantity_before: Optional[float]
quantity_after: Optional[float]
reference_number: Optional[str]
supplier_id: Optional[str]
notes: Optional[str]
reason_code: Optional[str]
movement_date: datetime
created_at: datetime
created_by: Optional[str]
# Related data
ingredient: Optional[IngredientResponse] = None
# ===== PRODUCT TRANSFORMATION SCHEMAS =====
class ProductTransformationCreate(InventoryBaseSchema):
"""Schema for creating product transformations"""
source_ingredient_id: str = Field(..., description="Source ingredient ID")
target_ingredient_id: str = Field(..., description="Target ingredient ID")
source_stage: ProductionStage = Field(..., description="Source production stage")
target_stage: ProductionStage = Field(..., description="Target production stage")
source_quantity: float = Field(..., gt=0, description="Input quantity")
target_quantity: float = Field(..., gt=0, description="Output quantity")
conversion_ratio: Optional[float] = Field(None, gt=0, description="Conversion ratio (auto-calculated if not provided)")
# Expiration handling
expiration_calculation_method: str = Field("days_from_transformation", description="How to calculate expiration")
expiration_days_offset: Optional[int] = Field(1, description="Days from transformation date for expiration")
# Process details
process_notes: Optional[str] = Field(None, description="Process notes")
target_batch_number: Optional[str] = Field(None, max_length=100, description="Target batch number")
# Source stock selection (optional - if not provided, uses FIFO)
source_stock_ids: Optional[List[str]] = Field(None, description="Specific source stock IDs to transform")
class ProductTransformationResponse(InventoryBaseSchema):
"""Schema for product transformation responses"""
id: str
tenant_id: str
transformation_reference: str
source_ingredient_id: str
target_ingredient_id: str
source_stage: ProductionStage
target_stage: ProductionStage
source_quantity: float
target_quantity: float
conversion_ratio: float
expiration_calculation_method: str
expiration_days_offset: Optional[int]
transformation_date: datetime
process_notes: Optional[str]
performed_by: Optional[str]
source_batch_numbers: Optional[str]
target_batch_number: Optional[str]
is_completed: bool
is_reversed: bool
created_at: datetime
created_by: Optional[str]
# Related data
source_ingredient: Optional[IngredientResponse] = None
target_ingredient: Optional[IngredientResponse] = None
# ===== ALERT SCHEMAS =====
class StockAlertResponse(InventoryBaseSchema):
"""Schema for stock alert API responses"""
id: str
tenant_id: str
ingredient_id: str
stock_id: Optional[str]
alert_type: str
severity: str
title: str
message: str
current_quantity: Optional[float]
threshold_value: Optional[float]
expiration_date: Optional[datetime]
is_active: bool
is_acknowledged: bool
acknowledged_by: Optional[str]
acknowledged_at: Optional[datetime]
is_resolved: bool
resolved_by: Optional[str]
resolved_at: Optional[datetime]
resolution_notes: Optional[str]
created_at: datetime
updated_at: datetime
# Related data
ingredient: Optional[IngredientResponse] = None
# ===== DASHBOARD AND SUMMARY SCHEMAS =====
class InventorySummary(InventoryBaseSchema):
"""Inventory dashboard summary"""
total_ingredients: int
total_stock_value: float
low_stock_alerts: int
expiring_soon_items: int
expired_items: int
out_of_stock_items: int
# By category
stock_by_category: Dict[str, Dict[str, Any]]
# Recent activity
recent_movements: int
recent_purchases: int
recent_waste: int
class StockLevelSummary(InventoryBaseSchema):
"""Stock level summary for an ingredient"""
ingredient_id: str
ingredient_name: str
unit_of_measure: str
total_quantity: float
available_quantity: float
reserved_quantity: float
# Status indicators
is_low_stock: bool
needs_reorder: bool
has_expired_stock: bool
# Batch information
total_batches: int
oldest_batch_date: Optional[datetime]
newest_batch_date: Optional[datetime]
next_expiration_date: Optional[datetime]
# Cost information
average_unit_cost: Optional[float]
total_stock_value: Optional[float]
# ===== REQUEST/RESPONSE WRAPPER SCHEMAS =====
class PaginatedResponse(BaseModel, Generic[T]):
"""Generic paginated response"""
items: List[T]
total: int
page: int
size: int
pages: int
class Config:
from_attributes = True
class InventoryFilter(BaseModel):
"""Inventory filtering parameters"""
category: Optional[IngredientCategory] = None
is_active: Optional[bool] = None
is_low_stock: Optional[bool] = None
needs_reorder: Optional[bool] = None
search: Optional[str] = None
class StockFilter(BaseModel):
"""Stock filtering parameters"""
ingredient_id: Optional[str] = None
production_stage: Optional[ProductionStage] = None
transformation_reference: Optional[str] = None
is_available: Optional[bool] = None
is_expired: Optional[bool] = None
expiring_within_days: Optional[int] = None
storage_location: Optional[str] = None
quality_status: Optional[str] = None
# Type aliases for paginated responses
IngredientListResponse = PaginatedResponse[IngredientResponse]
StockListResponse = PaginatedResponse[StockResponse]
StockMovementListResponse = PaginatedResponse[StockMovementResponse]
StockAlertListResponse = PaginatedResponse[StockAlertResponse]