607 lines
25 KiB
Python
607 lines
25 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 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
|
|
|
|
|
|
# ===== 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] |