Create new services: inventory, recipes, suppliers
This commit is contained in:
390
services/inventory/app/schemas/inventory.py
Normal file
390
services/inventory/app/schemas/inventory.py
Normal file
@@ -0,0 +1,390 @@
|
||||
# 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
|
||||
|
||||
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"""
|
||||
name: str = Field(..., max_length=255, description="Ingredient name")
|
||||
sku: Optional[str] = Field(None, max_length=100, description="SKU code")
|
||||
barcode: Optional[str] = Field(None, max_length=50, description="Barcode")
|
||||
category: IngredientCategory = Field(..., description="Ingredient 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")
|
||||
|
||||
# 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):
|
||||
"""Schema for updating ingredients"""
|
||||
name: Optional[str] = Field(None, max_length=255, description="Ingredient name")
|
||||
sku: Optional[str] = Field(None, max_length=100, description="SKU code")
|
||||
barcode: Optional[str] = Field(None, max_length=50, description="Barcode")
|
||||
category: Optional[IngredientCategory] = Field(None, description="Ingredient 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")
|
||||
|
||||
# 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):
|
||||
"""Schema for ingredient API responses"""
|
||||
id: str
|
||||
tenant_id: str
|
||||
name: str
|
||||
sku: Optional[str]
|
||||
barcode: Optional[str]
|
||||
category: IngredientCategory
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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]
|
||||
current_quantity: float
|
||||
reserved_quantity: float
|
||||
available_quantity: float
|
||||
received_date: Optional[datetime]
|
||||
expiration_date: Optional[datetime]
|
||||
best_before_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
|
||||
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
|
||||
|
||||
|
||||
# ===== 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
|
||||
is_available: Optional[bool] = None
|
||||
is_expired: Optional[bool] = None
|
||||
expiring_within_days: Optional[int] = None
|
||||
storage_location: Optional[str] = None
|
||||
quality_status: Optional[str] = None
|
||||
|
||||
|
||||
# Type aliases for paginated responses
|
||||
IngredientListResponse = PaginatedResponse[IngredientResponse]
|
||||
StockListResponse = PaginatedResponse[StockResponse]
|
||||
StockMovementListResponse = PaginatedResponse[StockMovementResponse]
|
||||
StockAlertListResponse = PaginatedResponse[StockAlertResponse]
|
||||
Reference in New Issue
Block a user