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