2025-08-13 17:39:35 +02:00
|
|
|
# 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
|
|
|
|
|
|
2025-09-17 16:06:30 +02:00
|
|
|
from app.models.inventory import UnitOfMeasure, IngredientCategory, StockMovementType, ProductType, ProductCategory, ProductionStage
|
2025-08-13 17:39:35 +02:00
|
|
|
|
|
|
|
|
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):
|
2025-08-17 15:21:10 +02:00
|
|
|
"""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)")
|
2025-08-13 17:39:35 +02:00
|
|
|
sku: Optional[str] = Field(None, max_length=100, description="SKU code")
|
|
|
|
|
barcode: Optional[str] = Field(None, max_length=50, description="Barcode")
|
2025-08-17 15:21:10 +02:00
|
|
|
category: Optional[str] = Field(None, description="Product category (ingredient or finished product category)")
|
2025-08-13 17:39:35 +02:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
shelf_life_days: Optional[int] = Field(None, gt=0, description="Shelf life in days")
|
|
|
|
|
storage_instructions: Optional[str] = Field(None, description="Storage instructions")
|
|
|
|
|
|
|
|
|
|
# Properties
|
|
|
|
|
is_perishable: bool = Field(False, description="Is perishable")
|
|
|
|
|
allergen_info: Optional[Dict[str, Any]] = Field(None, description="Allergen information")
|
|
|
|
|
|
|
|
|
|
@validator('storage_temperature_max')
|
|
|
|
|
def validate_temperature_range(cls, v, values):
|
|
|
|
|
if v is not None and 'storage_temperature_min' in values and values['storage_temperature_min'] is not None:
|
|
|
|
|
if v <= values['storage_temperature_min']:
|
|
|
|
|
raise ValueError('Max temperature must be greater than min temperature')
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
@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):
|
2025-08-17 15:21:10 +02:00
|
|
|
"""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)")
|
2025-08-13 17:39:35 +02:00
|
|
|
sku: Optional[str] = Field(None, max_length=100, description="SKU code")
|
|
|
|
|
barcode: Optional[str] = Field(None, max_length=50, description="Barcode")
|
2025-08-17 15:21:10 +02:00
|
|
|
category: Optional[str] = Field(None, description="Product category")
|
2025-08-13 17:39:35 +02:00
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
shelf_life_days: Optional[int] = Field(None, gt=0, description="Shelf life in days")
|
|
|
|
|
storage_instructions: Optional[str] = Field(None, description="Storage instructions")
|
|
|
|
|
|
|
|
|
|
# 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):
|
2025-08-17 15:21:10 +02:00
|
|
|
"""Schema for ingredient and finished product API responses"""
|
2025-08-13 17:39:35 +02:00
|
|
|
id: str
|
|
|
|
|
tenant_id: str
|
|
|
|
|
name: str
|
2025-08-17 15:21:10 +02:00
|
|
|
product_type: ProductType
|
2025-08-13 17:39:35 +02:00
|
|
|
sku: Optional[str]
|
|
|
|
|
barcode: Optional[str]
|
2025-08-17 15:21:10 +02:00
|
|
|
category: Optional[str] # Will be populated from ingredient_category or product_category
|
2025-08-13 17:39:35 +02:00
|
|
|
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]
|
|
|
|
|
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]
|
|
|
|
|
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")
|
|
|
|
|
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")
|
2025-09-17 16:06:30 +02:00
|
|
|
|
|
|
|
|
# Production stage tracking
|
|
|
|
|
production_stage: ProductionStage = Field(ProductionStage.RAW_INGREDIENT, description="Production stage of the stock")
|
|
|
|
|
transformation_reference: Optional[str] = Field(None, max_length=100, description="Transformation reference ID")
|
|
|
|
|
|
2025-08-13 17:39:35 +02:00
|
|
|
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")
|
2025-09-17 16:06:30 +02:00
|
|
|
|
|
|
|
|
# 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")
|
|
|
|
|
|
2025-08-13 17:39:35 +02:00
|
|
|
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")
|
2025-09-17 16:06:30 +02:00
|
|
|
|
2025-08-13 17:39:35 +02:00
|
|
|
quality_status: str = Field("good", description="Quality status")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StockUpdate(InventoryBaseSchema):
|
|
|
|
|
"""Schema for updating stock entries"""
|
|
|
|
|
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")
|
2025-09-17 16:06:30 +02:00
|
|
|
|
|
|
|
|
# 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")
|
|
|
|
|
|
2025-08-13 17:39:35 +02:00
|
|
|
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")
|
2025-09-17 16:06:30 +02:00
|
|
|
|
|
|
|
|
# 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")
|
|
|
|
|
|
2025-08-13 17:39:35 +02:00
|
|
|
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")
|
2025-09-17 16:06:30 +02:00
|
|
|
|
2025-08-13 17:39:35 +02:00
|
|
|
is_available: Optional[bool] = Field(None, description="Is available")
|
|
|
|
|
quality_status: Optional[str] = Field(None, description="Quality status")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StockResponse(InventoryBaseSchema):
|
|
|
|
|
"""Schema for stock API responses"""
|
|
|
|
|
id: str
|
|
|
|
|
tenant_id: str
|
|
|
|
|
ingredient_id: str
|
|
|
|
|
batch_number: Optional[str]
|
|
|
|
|
lot_number: Optional[str]
|
|
|
|
|
supplier_batch_ref: Optional[str]
|
2025-09-17 16:06:30 +02:00
|
|
|
|
|
|
|
|
# Production stage tracking
|
|
|
|
|
production_stage: ProductionStage
|
|
|
|
|
transformation_reference: Optional[str]
|
|
|
|
|
|
2025-08-13 17:39:35 +02:00
|
|
|
current_quantity: float
|
|
|
|
|
reserved_quantity: float
|
|
|
|
|
available_quantity: float
|
|
|
|
|
received_date: Optional[datetime]
|
|
|
|
|
expiration_date: Optional[datetime]
|
|
|
|
|
best_before_date: Optional[datetime]
|
2025-09-17 16:06:30 +02:00
|
|
|
|
|
|
|
|
# Stage-specific expiration fields
|
|
|
|
|
original_expiration_date: Optional[datetime]
|
|
|
|
|
transformation_date: Optional[datetime]
|
|
|
|
|
final_expiration_date: Optional[datetime]
|
|
|
|
|
|
2025-08-13 17:39:35 +02:00
|
|
|
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
|
|
|
|
|
created_at: datetime
|
|
|
|
|
updated_at: datetime
|
2025-09-17 16:06:30 +02:00
|
|
|
|
2025-08-13 17:39:35 +02:00
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
2025-09-17 16:06:30 +02:00
|
|
|
# ===== 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
|
|
|
|
|
|
|
|
|
|
|
2025-08-13 17:39:35 +02:00
|
|
|
# ===== 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
|
2025-09-17 16:06:30 +02:00
|
|
|
production_stage: Optional[ProductionStage] = None
|
|
|
|
|
transformation_reference: Optional[str] = None
|
2025-08-13 17:39:35 +02:00
|
|
|
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]
|