Initial commit - production deployment
This commit is contained in:
631
services/inventory/app/schemas/inventory.py
Normal file
631
services/inventory/app/schemas/inventory.py
Normal file
@@ -0,0 +1,631 @@
|
||||
# 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 uuid import UUID
|
||||
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
|
||||
# Note: average_cost is calculated automatically from purchases (not set on create)
|
||||
# All cost fields are optional - can be added later after onboarding
|
||||
standard_cost: Optional[Decimal] = Field(None, ge=0, description="Standard/target cost per unit for budgeting")
|
||||
|
||||
# Stock management - all optional with sensible defaults for onboarding
|
||||
# These can be configured later based on actual usage patterns
|
||||
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_perishable: bool = Field(False, description="Is perishable")
|
||||
allergen_info: Optional[Dict[str, Any]] = Field(None, description="Allergen information")
|
||||
|
||||
# NEW: Local production support
|
||||
produced_locally: bool = Field(False, description="If true, ingredient is produced in-house")
|
||||
recipe_id: Optional[str] = Field(None, description="Recipe ID for BOM explosion (if produced locally)")
|
||||
|
||||
@validator('reorder_point')
|
||||
def validate_reorder_point(cls, v, values):
|
||||
# Only validate if both values are provided and not None
|
||||
low_stock = values.get('low_stock_threshold')
|
||||
if v is not None and low_stock is not None:
|
||||
try:
|
||||
if v <= low_stock:
|
||||
raise ValueError('Reorder point must be greater than low stock threshold')
|
||||
except TypeError:
|
||||
# Skip validation if comparison fails due to type mismatch
|
||||
pass
|
||||
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")
|
||||
|
||||
# NEW: Local production support
|
||||
produced_locally: Optional[bool] = Field(None, description="If true, ingredient is produced in-house")
|
||||
recipe_id: Optional[str] = Field(None, description="Recipe ID for BOM explosion (if produced locally)")
|
||||
|
||||
|
||||
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: Optional[float] # Now optional
|
||||
reorder_point: Optional[float] # Now optional
|
||||
reorder_quantity: Optional[float] # Now optional
|
||||
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]]
|
||||
|
||||
# NEW: Local production support
|
||||
produced_locally: bool = False
|
||||
recipe_id: Optional[str] = None
|
||||
|
||||
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
|
||||
|
||||
@validator('allergen_info', pre=True)
|
||||
def validate_allergen_info(cls, v):
|
||||
"""Convert empty lists or lists to empty dict, handle None"""
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, list):
|
||||
# If it's an empty list, return None; if it's a non-empty list, convert to dict format
|
||||
return {"allergens": v} if v else None
|
||||
if isinstance(v, dict):
|
||||
return v
|
||||
# For any other type including invalid ones, return None
|
||||
return None
|
||||
|
||||
|
||||
# ===== BULK INGREDIENT SCHEMAS =====
|
||||
|
||||
class BulkIngredientCreate(InventoryBaseSchema):
|
||||
"""Schema for bulk creating ingredients"""
|
||||
ingredients: List[IngredientCreate] = Field(..., description="List of ingredients to create")
|
||||
|
||||
|
||||
class BulkIngredientResult(InventoryBaseSchema):
|
||||
"""Schema for individual result in bulk operation"""
|
||||
index: int = Field(..., description="Index of the ingredient in the original request")
|
||||
success: bool = Field(..., description="Whether the creation succeeded")
|
||||
ingredient: Optional[IngredientResponse] = Field(None, description="Created ingredient (if successful)")
|
||||
error: Optional[str] = Field(None, description="Error message (if failed)")
|
||||
|
||||
|
||||
class BulkIngredientResponse(InventoryBaseSchema):
|
||||
"""Schema for bulk ingredient creation response"""
|
||||
total_requested: int = Field(..., description="Total number of ingredients requested")
|
||||
total_created: int = Field(..., description="Number of ingredients successfully created")
|
||||
total_failed: int = Field(..., description="Number of ingredients that failed")
|
||||
results: List[BulkIngredientResult] = Field(..., description="Detailed results for each ingredient")
|
||||
transaction_id: str = Field(..., description="Transaction ID for audit trail")
|
||||
|
||||
|
||||
# ===== 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('ingredient_id')
|
||||
def validate_ingredient_id(cls, v):
|
||||
"""Validate ingredient_id is a valid UUID"""
|
||||
if not v:
|
||||
raise ValueError("ingredient_id is required")
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
# Validate it's a proper UUID
|
||||
UUID(v)
|
||||
return v
|
||||
except (ValueError, AttributeError) as e:
|
||||
raise ValueError(f"ingredient_id must be a valid UUID string, got: {v}")
|
||||
return str(v)
|
||||
|
||||
@validator('supplier_id')
|
||||
def validate_supplier_id(cls, v):
|
||||
"""Convert empty string to None for optional UUID field"""
|
||||
if v == '' or (isinstance(v, str) and v.strip() == ''):
|
||||
return None
|
||||
return v
|
||||
|
||||
@validator('storage_temperature_max')
|
||||
def validate_temperature_range(cls, v, values):
|
||||
# Only validate if both values are provided and not None
|
||||
min_temp = values.get('storage_temperature_min')
|
||||
if v is not None and min_temp is not None:
|
||||
try:
|
||||
if v <= min_temp:
|
||||
raise ValueError('Max temperature must be greater than min temperature')
|
||||
except TypeError:
|
||||
# Skip validation if comparison fails due to type mismatch
|
||||
pass
|
||||
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")
|
||||
|
||||
@validator('supplier_id')
|
||||
def validate_supplier_id(cls, v):
|
||||
"""Convert empty string to None for optional UUID field"""
|
||||
if v == '' or (isinstance(v, str) and v.strip() == ''):
|
||||
return None
|
||||
return v
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# ===== BULK STOCK SCHEMAS =====
|
||||
|
||||
class BulkStockCreate(InventoryBaseSchema):
|
||||
"""Schema for bulk creating stock entries"""
|
||||
stocks: List[StockCreate] = Field(..., description="List of stock entries to create")
|
||||
|
||||
|
||||
class BulkStockResult(InventoryBaseSchema):
|
||||
"""Schema for individual result in bulk stock operation"""
|
||||
index: int = Field(..., description="Index of the stock in the original request")
|
||||
success: bool = Field(..., description="Whether the creation succeeded")
|
||||
stock: Optional[StockResponse] = Field(None, description="Created stock (if successful)")
|
||||
error: Optional[str] = Field(None, description="Error message (if failed)")
|
||||
|
||||
|
||||
class BulkStockResponse(InventoryBaseSchema):
|
||||
"""Schema for bulk stock creation response"""
|
||||
total_requested: int = Field(..., description="Total number of stock entries requested")
|
||||
total_created: int = Field(..., description="Number of stock entries successfully created")
|
||||
total_failed: int = Field(..., description="Number of stock entries that failed")
|
||||
results: List[BulkStockResult] = Field(..., description="Detailed results for each stock entry")
|
||||
transaction_id: str = Field(..., description="Transaction ID for audit trail")
|
||||
|
||||
|
||||
# ===== 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")
|
||||
|
||||
@validator('ingredient_id')
|
||||
def validate_ingredient_id(cls, v):
|
||||
"""Validate ingredient_id is a valid UUID"""
|
||||
if not v:
|
||||
raise ValueError("ingredient_id is required")
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
# Validate it's a proper UUID
|
||||
UUID(v)
|
||||
return v
|
||||
except (ValueError, AttributeError) as e:
|
||||
raise ValueError(f"ingredient_id must be a valid UUID string, got: {v}")
|
||||
return str(v)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
@validator('source_ingredient_id', 'target_ingredient_id')
|
||||
def validate_ingredient_ids(cls, v):
|
||||
"""Validate ingredient IDs are valid UUIDs"""
|
||||
if not v:
|
||||
raise ValueError("ingredient_id is required")
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
# Validate it's a proper UUID
|
||||
UUID(v)
|
||||
return v
|
||||
except (ValueError, AttributeError) as e:
|
||||
raise ValueError(f"ingredient_id must be a valid UUID string, got: {v}")
|
||||
return str(v)
|
||||
|
||||
|
||||
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]
|
||||
Reference in New Issue
Block a user