Files
bakery-ia/services/inventory/app/schemas/inventory.py

631 lines
26 KiB
Python
Raw Normal View History

# 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
2025-11-13 16:01:08 +01:00
from uuid import UUID
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
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)")
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)")
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")
2025-10-27 16:33:26 +01:00
# Pricing
2025-10-27 16:33:26 +01:00
# Note: average_cost is calculated automatically from purchases (not set on create)
2025-11-09 09:22:08 +01:00
# All cost fields are optional - can be added later after onboarding
2025-10-27 16:33:26 +01:00
standard_cost: Optional[Decimal] = Field(None, ge=0, description="Standard/target cost per unit for budgeting")
2025-11-09 09:22:08 +01:00
# 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")
2025-09-18 08:06:32 +02:00
# 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")
2025-10-30 21:08:07 +01:00
# 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):
2025-11-09 09:22:08 +01:00
# 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):
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)")
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")
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")
2025-09-18 08:06:32 +02:00
# 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")
2025-10-30 21:08:07 +01:00
# 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):
2025-08-17 15:21:10 +02:00
"""Schema for ingredient and finished product API responses"""
id: str
tenant_id: str
name: str
2025-08-17 15:21:10 +02:00
product_type: ProductType
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
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]
2025-11-09 09:22:08 +01:00
low_stock_threshold: Optional[float] # Now optional
reorder_point: Optional[float] # Now optional
reorder_quantity: Optional[float] # Now optional
max_stock_level: Optional[float]
2025-09-18 08:06:32 +02:00
shelf_life_days: Optional[int] # Default value only
is_active: bool
is_perishable: bool
allergen_info: Optional[Dict[str, Any]]
2025-10-30 21:08:07 +01:00
# 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):
2025-11-13 16:01:08 +01:00
# 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
2025-11-13 16:01:08 +01:00
# For any other type including invalid ones, return None
return None
2025-12-25 18:35:37 +01:00
# ===== 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")
2025-09-17 16:06:30 +02:00
# Production stage tracking
2025-09-18 08:06:32 +02:00
production_stage: ProductionStage = Field(default=ProductionStage.RAW_INGREDIENT, description="Production stage of the stock")
2025-09-17 16:06:30 +02:00
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")
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")
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
quality_status: str = Field("good", description="Quality status")
2025-09-18 08:06:32 +02:00
# 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")
2025-11-13 16:01:08 +01:00
@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')
2025-10-27 16:33:26 +01:00
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):
2025-11-09 09:22:08 +01:00
# Only validate if both values are provided and not None
min_temp = values.get('storage_temperature_min')
2025-11-09 09:22:08 +01:00
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")
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")
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")
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
is_available: Optional[bool] = Field(None, description="Is available")
quality_status: Optional[str] = Field(None, description="Quality status")
2025-09-18 08:06:32 +02:00
# 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")
2025-11-13 16:01:08 +01:00
@validator('supplier_id')
2025-10-27 16:33:26 +01:00
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]
2025-09-17 16:06:30 +02:00
# 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]
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]
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
2025-09-18 08:06:32 +02:00
# 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
2025-09-17 16:06:30 +02:00
# Related data
ingredient: Optional[IngredientResponse] = None
2026-01-01 19:01:33 +01:00
# ===== 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")
2025-11-13 16:01:08 +01:00
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")
2025-11-13 16:01:08 +01:00
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")
2025-11-13 16:01:08 +01:00
@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
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")
2025-11-13 16:01:08 +01:00
@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)
2025-09-17 16:06:30 +02:00
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
2025-09-17 16:06:30 +02:00
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]