Initial commit - production deployment
This commit is contained in:
0
services/inventory/app/schemas/__init__.py
Normal file
0
services/inventory/app/schemas/__init__.py
Normal file
250
services/inventory/app/schemas/dashboard.py
Normal file
250
services/inventory/app/schemas/dashboard.py
Normal file
@@ -0,0 +1,250 @@
|
||||
# ================================================================
|
||||
# services/inventory/app/schemas/dashboard.py
|
||||
# ================================================================
|
||||
"""
|
||||
Dashboard and analytics schemas for Inventory Service
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ===== Dashboard Summary Schemas =====
|
||||
|
||||
class InventoryDashboardSummary(BaseModel):
|
||||
"""Comprehensive dashboard summary for inventory management"""
|
||||
|
||||
# Current inventory metrics
|
||||
total_ingredients: int
|
||||
active_ingredients: int
|
||||
total_stock_value: Decimal
|
||||
total_stock_items: int
|
||||
|
||||
# Stock status breakdown
|
||||
in_stock_items: int
|
||||
low_stock_items: int
|
||||
out_of_stock_items: int
|
||||
expired_items: int
|
||||
expiring_soon_items: int
|
||||
|
||||
# Food safety metrics
|
||||
food_safety_alerts_active: int
|
||||
temperature_violations_today: int
|
||||
compliance_issues: int
|
||||
certifications_expiring_soon: int
|
||||
|
||||
# Recent activity
|
||||
recent_stock_movements: int
|
||||
recent_purchases: int
|
||||
recent_waste: int
|
||||
recent_adjustments: int
|
||||
|
||||
# Business model context
|
||||
business_model: Optional[str] = None # individual_bakery, central_bakery
|
||||
business_model_confidence: Optional[Decimal] = None
|
||||
|
||||
# Category breakdown
|
||||
stock_by_category: Dict[str, Any]
|
||||
alerts_by_severity: Dict[str, int]
|
||||
movements_by_type: Dict[str, int]
|
||||
|
||||
# Performance indicators
|
||||
inventory_turnover_ratio: Optional[Decimal] = None
|
||||
waste_percentage: Optional[Decimal] = None
|
||||
compliance_score: Optional[Decimal] = None
|
||||
cost_per_unit_avg: Optional[Decimal] = None
|
||||
|
||||
# Trending data
|
||||
stock_value_trend: List[Dict[str, Any]] = []
|
||||
alert_trend: List[Dict[str, Any]] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class StockStatusSummary(BaseModel):
|
||||
"""Summary of stock status by category"""
|
||||
category: str
|
||||
total_ingredients: int
|
||||
in_stock: int
|
||||
low_stock: int
|
||||
out_of_stock: int
|
||||
total_value: Decimal
|
||||
percentage_of_total: Decimal
|
||||
|
||||
|
||||
class AlertSummary(BaseModel):
|
||||
"""Summary of alerts by type and severity"""
|
||||
alert_type: str
|
||||
severity: str
|
||||
count: int
|
||||
oldest_alert_age_hours: Optional[int] = None
|
||||
average_resolution_time_hours: Optional[int] = None
|
||||
|
||||
|
||||
class RecentActivity(BaseModel):
|
||||
"""Recent activity item for dashboard"""
|
||||
activity_type: str # stock_added, stock_consumed, alert_created, etc.
|
||||
description: str
|
||||
timestamp: datetime
|
||||
user_name: Optional[str] = None
|
||||
impact_level: str = Field(default="low") # low, medium, high
|
||||
entity_id: Optional[UUID] = None
|
||||
entity_type: Optional[str] = None
|
||||
|
||||
|
||||
# ===== Food Safety Dashboard Schemas =====
|
||||
|
||||
class FoodSafetyDashboard(BaseModel):
|
||||
"""Food safety specific dashboard metrics"""
|
||||
|
||||
# Compliance overview
|
||||
total_compliance_items: int
|
||||
compliant_items: int
|
||||
non_compliant_items: int
|
||||
pending_review_items: int
|
||||
compliance_percentage: Decimal
|
||||
|
||||
# Temperature monitoring
|
||||
temperature_sensors_online: int
|
||||
temperature_sensors_total: int
|
||||
temperature_violations_24h: int
|
||||
current_temperature_status: str # all_good, warnings, violations
|
||||
|
||||
# Expiration tracking
|
||||
items_expiring_today: int
|
||||
items_expiring_this_week: int
|
||||
expired_items_requiring_action: int
|
||||
|
||||
# Audit and certification status
|
||||
upcoming_audits: int
|
||||
overdue_audits: int
|
||||
certifications_valid: int
|
||||
certifications_expiring_soon: int
|
||||
|
||||
# Risk assessment
|
||||
high_risk_items: int
|
||||
critical_alerts: int
|
||||
regulatory_notifications_pending: int
|
||||
|
||||
# Recent safety events
|
||||
recent_safety_incidents: List[RecentActivity] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TemperatureMonitoringStatus(BaseModel):
|
||||
"""Current temperature monitoring status"""
|
||||
location: str
|
||||
equipment_id: Optional[str] = None
|
||||
current_temperature: Decimal
|
||||
target_min: Decimal
|
||||
target_max: Decimal
|
||||
status: str # normal, warning, critical
|
||||
last_reading: datetime
|
||||
hours_since_last_reading: Decimal
|
||||
alert_active: bool = False
|
||||
|
||||
|
||||
class ComplianceStatusSummary(BaseModel):
|
||||
"""Compliance status summary by standard"""
|
||||
standard: str
|
||||
standard_name: str
|
||||
total_items: int
|
||||
compliant: int
|
||||
non_compliant: int
|
||||
pending_review: int
|
||||
expired: int
|
||||
compliance_rate: Decimal
|
||||
next_audit_date: Optional[datetime] = None
|
||||
|
||||
|
||||
# ===== Analytics and Reporting Schemas =====
|
||||
|
||||
class InventoryAnalytics(BaseModel):
|
||||
"""Advanced analytics for inventory management"""
|
||||
|
||||
# Turnover analysis
|
||||
inventory_turnover_rate: Decimal
|
||||
fast_moving_items: List[Dict[str, Any]]
|
||||
slow_moving_items: List[Dict[str, Any]]
|
||||
dead_stock_items: List[Dict[str, Any]]
|
||||
|
||||
# Cost analysis
|
||||
total_inventory_cost: Decimal
|
||||
cost_by_category: Dict[str, Decimal]
|
||||
average_unit_cost_trend: List[Dict[str, Any]]
|
||||
waste_cost_analysis: Dict[str, Any]
|
||||
|
||||
# Efficiency metrics
|
||||
stockout_frequency: Dict[str, int]
|
||||
overstock_frequency: Dict[str, int]
|
||||
reorder_accuracy: Decimal
|
||||
forecast_accuracy: Decimal
|
||||
|
||||
# Quality and safety metrics
|
||||
quality_incidents_rate: Decimal
|
||||
food_safety_score: Decimal
|
||||
compliance_score_by_standard: Dict[str, Decimal]
|
||||
temperature_compliance_rate: Decimal
|
||||
|
||||
# Supplier performance
|
||||
supplier_performance: List[Dict[str, Any]]
|
||||
delivery_reliability: Decimal
|
||||
quality_consistency: Decimal
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class BusinessModelInsights(BaseModel):
|
||||
"""Business model insights based on inventory patterns"""
|
||||
detected_model: str # individual_bakery, central_bakery, mixed
|
||||
confidence_score: Decimal
|
||||
|
||||
# Model characteristics
|
||||
total_ingredient_types: int
|
||||
average_stock_per_ingredient: Decimal
|
||||
finished_product_ratio: Decimal
|
||||
supplier_diversity: int
|
||||
|
||||
# Operational patterns
|
||||
order_frequency_pattern: str
|
||||
seasonal_variation: bool
|
||||
bulk_purchasing_indicator: Decimal
|
||||
production_scale_indicator: str
|
||||
|
||||
# Recommendations
|
||||
business_model_specific_recommendations: List[str]
|
||||
optimization_opportunities: List[str]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ===== Request/Filter Schemas =====
|
||||
|
||||
class DashboardFilter(BaseModel):
|
||||
"""Filtering options for dashboard data"""
|
||||
date_from: Optional[datetime] = None
|
||||
date_to: Optional[datetime] = None
|
||||
categories: Optional[List[str]] = None
|
||||
severity_levels: Optional[List[str]] = None
|
||||
alert_types: Optional[List[str]] = None
|
||||
business_model: Optional[str] = None
|
||||
include_inactive: bool = False
|
||||
|
||||
|
||||
class AlertsFilter(BaseModel):
|
||||
"""Filtering options for alerts dashboard"""
|
||||
alert_types: Optional[List[str]] = None
|
||||
severities: Optional[List[str]] = None
|
||||
statuses: Optional[List[str]] = None
|
||||
date_from: Optional[datetime] = None
|
||||
date_to: Optional[datetime] = None
|
||||
assigned_to: Optional[UUID] = None
|
||||
unresolved_only: bool = True
|
||||
283
services/inventory/app/schemas/food_safety.py
Normal file
283
services/inventory/app/schemas/food_safety.py
Normal file
@@ -0,0 +1,283 @@
|
||||
# ================================================================
|
||||
# services/inventory/app/schemas/food_safety.py
|
||||
# ================================================================
|
||||
"""
|
||||
Food safety schemas for Inventory Service
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
|
||||
# ===== Food Safety Compliance Schemas =====
|
||||
|
||||
class FoodSafetyComplianceBase(BaseModel):
|
||||
ingredient_id: UUID
|
||||
standard: str
|
||||
compliance_status: str = Field(default="pending_review")
|
||||
certification_number: Optional[str] = None
|
||||
certifying_body: Optional[str] = None
|
||||
certification_date: Optional[datetime] = None
|
||||
expiration_date: Optional[datetime] = None
|
||||
requirements: Optional[Dict[str, Any]] = None
|
||||
compliance_notes: Optional[str] = None
|
||||
documentation_url: Optional[str] = None
|
||||
last_audit_date: Optional[datetime] = None
|
||||
next_audit_date: Optional[datetime] = None
|
||||
auditor_name: Optional[str] = None
|
||||
audit_score: Optional[float] = Field(None, ge=0, le=100)
|
||||
risk_level: str = Field(default="medium")
|
||||
risk_factors: Optional[List[str]] = None
|
||||
mitigation_measures: Optional[List[str]] = None
|
||||
requires_monitoring: bool = Field(default=True)
|
||||
monitoring_frequency_days: Optional[int] = Field(None, gt=0)
|
||||
|
||||
|
||||
class FoodSafetyComplianceCreate(FoodSafetyComplianceBase):
|
||||
tenant_id: UUID
|
||||
|
||||
|
||||
class FoodSafetyComplianceUpdate(BaseModel):
|
||||
compliance_status: Optional[str] = None
|
||||
certification_number: Optional[str] = None
|
||||
certifying_body: Optional[str] = None
|
||||
certification_date: Optional[datetime] = None
|
||||
expiration_date: Optional[datetime] = None
|
||||
requirements: Optional[Dict[str, Any]] = None
|
||||
compliance_notes: Optional[str] = None
|
||||
documentation_url: Optional[str] = None
|
||||
last_audit_date: Optional[datetime] = None
|
||||
next_audit_date: Optional[datetime] = None
|
||||
auditor_name: Optional[str] = None
|
||||
audit_score: Optional[float] = Field(None, ge=0, le=100)
|
||||
risk_level: Optional[str] = None
|
||||
risk_factors: Optional[List[str]] = None
|
||||
mitigation_measures: Optional[List[str]] = None
|
||||
requires_monitoring: Optional[bool] = None
|
||||
monitoring_frequency_days: Optional[int] = Field(None, gt=0)
|
||||
|
||||
|
||||
class FoodSafetyComplianceResponse(FoodSafetyComplianceBase):
|
||||
id: UUID
|
||||
tenant_id: UUID
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
created_by: Optional[UUID] = None
|
||||
updated_by: Optional[UUID] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ===== Temperature Monitoring Schemas =====
|
||||
|
||||
class TemperatureLogBase(BaseModel):
|
||||
storage_location: str = Field(..., min_length=1, max_length=100)
|
||||
warehouse_zone: Optional[str] = Field(None, max_length=50)
|
||||
equipment_id: Optional[str] = Field(None, max_length=100)
|
||||
temperature_celsius: float
|
||||
humidity_percentage: Optional[float] = Field(None, ge=0, le=100)
|
||||
target_temperature_min: Optional[float] = None
|
||||
target_temperature_max: Optional[float] = None
|
||||
measurement_method: str = Field(default="manual")
|
||||
device_id: Optional[str] = Field(None, max_length=100)
|
||||
calibration_date: Optional[datetime] = None
|
||||
|
||||
|
||||
class TemperatureLogCreate(TemperatureLogBase):
|
||||
tenant_id: UUID
|
||||
|
||||
|
||||
class TemperatureLogResponse(TemperatureLogBase):
|
||||
id: UUID
|
||||
tenant_id: UUID
|
||||
is_within_range: bool
|
||||
alert_triggered: bool
|
||||
deviation_minutes: Optional[int] = None
|
||||
recorded_at: datetime
|
||||
created_at: datetime
|
||||
recorded_by: Optional[UUID] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ===== Food Safety Alert Schemas =====
|
||||
|
||||
class FoodSafetyAlertBase(BaseModel):
|
||||
alert_type: str
|
||||
severity: str = Field(default="medium")
|
||||
risk_level: str = Field(default="medium")
|
||||
source_entity_type: str
|
||||
source_entity_id: UUID
|
||||
ingredient_id: Optional[UUID] = None
|
||||
stock_id: Optional[UUID] = None
|
||||
title: str = Field(..., min_length=1, max_length=200)
|
||||
description: str = Field(..., min_length=1)
|
||||
detailed_message: Optional[str] = None
|
||||
regulatory_requirement: Optional[str] = Field(None, max_length=100)
|
||||
compliance_standard: Optional[str] = None
|
||||
regulatory_action_required: bool = Field(default=False)
|
||||
trigger_condition: Optional[str] = Field(None, max_length=200)
|
||||
threshold_value: Optional[Decimal] = None
|
||||
actual_value: Optional[Decimal] = None
|
||||
alert_data: Optional[Dict[str, Any]] = None
|
||||
environmental_factors: Optional[Dict[str, Any]] = None
|
||||
affected_products: Optional[List[UUID]] = None
|
||||
public_health_risk: bool = Field(default=False)
|
||||
business_impact: Optional[str] = None
|
||||
estimated_loss: Optional[Decimal] = Field(None, ge=0)
|
||||
|
||||
|
||||
class FoodSafetyAlertCreate(FoodSafetyAlertBase):
|
||||
tenant_id: UUID
|
||||
alert_code: str = Field(..., min_length=1, max_length=50)
|
||||
|
||||
|
||||
class FoodSafetyAlertUpdate(BaseModel):
|
||||
status: Optional[str] = None
|
||||
alert_state: Optional[str] = None
|
||||
immediate_actions_taken: Optional[List[str]] = None
|
||||
investigation_notes: Optional[str] = None
|
||||
resolution_action: Optional[str] = Field(None, max_length=200)
|
||||
resolution_notes: Optional[str] = None
|
||||
corrective_actions: Optional[List[str]] = None
|
||||
preventive_measures: Optional[List[str]] = None
|
||||
assigned_to: Optional[UUID] = None
|
||||
assigned_role: Optional[str] = Field(None, max_length=50)
|
||||
escalated_to: Optional[UUID] = None
|
||||
escalation_deadline: Optional[datetime] = None
|
||||
documentation: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class FoodSafetyAlertResponse(FoodSafetyAlertBase):
|
||||
id: UUID
|
||||
tenant_id: UUID
|
||||
alert_code: str
|
||||
status: str
|
||||
alert_state: str
|
||||
immediate_actions_taken: Optional[List[str]] = None
|
||||
investigation_notes: Optional[str] = None
|
||||
resolution_action: Optional[str] = None
|
||||
resolution_notes: Optional[str] = None
|
||||
corrective_actions: Optional[List[str]] = None
|
||||
preventive_measures: Optional[List[str]] = None
|
||||
first_occurred_at: datetime
|
||||
last_occurred_at: datetime
|
||||
acknowledged_at: Optional[datetime] = None
|
||||
resolved_at: Optional[datetime] = None
|
||||
escalation_deadline: Optional[datetime] = None
|
||||
occurrence_count: int
|
||||
is_recurring: bool
|
||||
recurrence_pattern: Optional[str] = None
|
||||
assigned_to: Optional[UUID] = None
|
||||
assigned_role: Optional[str] = None
|
||||
escalated_to: Optional[UUID] = None
|
||||
escalation_level: int
|
||||
notification_sent: bool
|
||||
notification_methods: Optional[List[str]] = None
|
||||
notification_recipients: Optional[List[str]] = None
|
||||
regulatory_notification_required: bool
|
||||
regulatory_notification_sent: bool
|
||||
documentation: Optional[Dict[str, Any]] = None
|
||||
audit_trail: Optional[List[Dict[str, Any]]] = None
|
||||
external_reference: Optional[str] = None
|
||||
detection_time: Optional[datetime] = None
|
||||
response_time_minutes: Optional[int] = None
|
||||
resolution_time_minutes: Optional[int] = None
|
||||
alert_accuracy: Optional[bool] = None
|
||||
false_positive: bool
|
||||
feedback_notes: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
created_by: Optional[UUID] = None
|
||||
updated_by: Optional[UUID] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ===== Bulk Operations Schemas =====
|
||||
|
||||
class BulkTemperatureLogCreate(BaseModel):
|
||||
"""Schema for bulk temperature logging"""
|
||||
tenant_id: UUID
|
||||
readings: List[TemperatureLogBase] = Field(..., min_items=1, max_items=100)
|
||||
|
||||
|
||||
class BulkComplianceUpdate(BaseModel):
|
||||
"""Schema for bulk compliance updates"""
|
||||
tenant_id: UUID
|
||||
updates: List[Dict[str, Any]] = Field(..., min_items=1, max_items=50)
|
||||
|
||||
|
||||
# ===== Filter and Query Schemas =====
|
||||
|
||||
class FoodSafetyFilter(BaseModel):
|
||||
"""Filtering options for food safety data"""
|
||||
compliance_standards: Optional[List[str]] = None
|
||||
compliance_statuses: Optional[List[str]] = None
|
||||
risk_levels: Optional[List[str]] = None
|
||||
alert_types: Optional[List[str]] = None
|
||||
severities: Optional[List[str]] = None
|
||||
date_from: Optional[datetime] = None
|
||||
date_to: Optional[datetime] = None
|
||||
assigned_to: Optional[UUID] = None
|
||||
include_resolved: bool = False
|
||||
regulatory_action_required: Optional[bool] = None
|
||||
|
||||
|
||||
class TemperatureMonitoringFilter(BaseModel):
|
||||
"""Filtering options for temperature monitoring"""
|
||||
storage_locations: Optional[List[str]] = None
|
||||
equipment_ids: Optional[List[str]] = None
|
||||
date_from: Optional[datetime] = None
|
||||
date_to: Optional[datetime] = None
|
||||
violations_only: bool = False
|
||||
alerts_only: bool = False
|
||||
|
||||
|
||||
# ===== Analytics Schemas =====
|
||||
|
||||
class FoodSafetyMetrics(BaseModel):
|
||||
"""Food safety performance metrics"""
|
||||
compliance_rate: Decimal = Field(..., ge=0, le=100)
|
||||
temperature_compliance_rate: Decimal = Field(..., ge=0, le=100)
|
||||
alert_response_time_avg: Optional[Decimal] = None
|
||||
alert_resolution_time_avg: Optional[Decimal] = None
|
||||
recurring_issues_count: int
|
||||
regulatory_violations: int
|
||||
certification_coverage: Decimal = Field(..., ge=0, le=100)
|
||||
audit_score_avg: Optional[Decimal] = Field(None, ge=0, le=100)
|
||||
risk_score: Decimal = Field(..., ge=0, le=10)
|
||||
|
||||
|
||||
class TemperatureAnalytics(BaseModel):
|
||||
"""Temperature monitoring analytics"""
|
||||
total_readings: int
|
||||
violations_count: int
|
||||
violation_rate: Decimal = Field(..., ge=0, le=100)
|
||||
average_temperature: Decimal
|
||||
temperature_range: Dict[str, Decimal]
|
||||
longest_violation_hours: Optional[int] = None
|
||||
equipment_performance: List[Dict[str, Any]]
|
||||
location_performance: List[Dict[str, Any]]
|
||||
|
||||
|
||||
# ===== Notification Schemas =====
|
||||
|
||||
class AlertNotificationPreferences(BaseModel):
|
||||
"""User preferences for alert notifications"""
|
||||
email_enabled: bool = True
|
||||
sms_enabled: bool = False
|
||||
whatsapp_enabled: bool = False
|
||||
dashboard_enabled: bool = True
|
||||
severity_threshold: str = Field(default="medium") # Only notify for this severity and above
|
||||
alert_types: Optional[List[str]] = None # Specific alert types to receive
|
||||
quiet_hours_start: Optional[str] = Field(None, pattern=r"^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$")
|
||||
quiet_hours_end: Optional[str] = Field(None, pattern=r"^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$")
|
||||
weekend_notifications: bool = True
|
||||
631
services/inventory/app/schemas/inventory.py
Normal file
631
services/inventory/app/schemas/inventory.py
Normal file
@@ -0,0 +1,631 @@
|
||||
# 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
|
||||
|
||||
|
||||
# ===== 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")
|
||||
|
||||
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]
|
||||
217
services/inventory/app/schemas/sustainability.py
Normal file
217
services/inventory/app/schemas/sustainability.py
Normal file
@@ -0,0 +1,217 @@
|
||||
# ================================================================
|
||||
# services/inventory/app/schemas/sustainability.py
|
||||
# ================================================================
|
||||
"""
|
||||
Sustainability Schemas - Environmental Impact & SDG Compliance
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PeriodInfo(BaseModel):
|
||||
"""Time period for metrics"""
|
||||
start_date: str
|
||||
end_date: str
|
||||
days: int
|
||||
|
||||
|
||||
class WasteMetrics(BaseModel):
|
||||
"""Waste tracking metrics"""
|
||||
total_waste_kg: float = Field(description="Total waste in kilograms")
|
||||
production_waste_kg: float = Field(description="Waste from production processes")
|
||||
expired_waste_kg: float = Field(description="Waste from expired inventory")
|
||||
waste_percentage: float = Field(description="Waste as percentage of total production")
|
||||
waste_by_reason: Dict[str, float] = Field(description="Breakdown by waste reason")
|
||||
|
||||
|
||||
class CO2Emissions(BaseModel):
|
||||
"""CO2 emission metrics"""
|
||||
kg: float = Field(description="CO2 emissions in kilograms")
|
||||
tons: float = Field(description="CO2 emissions in tons")
|
||||
trees_to_offset: float = Field(description="Equivalent trees needed to offset emissions")
|
||||
|
||||
|
||||
class WaterFootprint(BaseModel):
|
||||
"""Water usage metrics"""
|
||||
liters: float = Field(description="Water footprint in liters")
|
||||
cubic_meters: float = Field(description="Water footprint in cubic meters")
|
||||
|
||||
|
||||
class LandUse(BaseModel):
|
||||
"""Land use metrics"""
|
||||
square_meters: float = Field(description="Land use in square meters")
|
||||
hectares: float = Field(description="Land use in hectares")
|
||||
|
||||
|
||||
class HumanEquivalents(BaseModel):
|
||||
"""Human-relatable equivalents for impact"""
|
||||
car_km_equivalent: float = Field(description="Equivalent kilometers driven by car")
|
||||
smartphone_charges: float = Field(description="Equivalent smartphone charges")
|
||||
showers_equivalent: float = Field(description="Equivalent showers taken")
|
||||
trees_planted: float = Field(description="Equivalent trees planted")
|
||||
|
||||
|
||||
class EnvironmentalImpact(BaseModel):
|
||||
"""Environmental impact of food waste"""
|
||||
co2_emissions: CO2Emissions
|
||||
water_footprint: WaterFootprint
|
||||
land_use: LandUse
|
||||
human_equivalents: HumanEquivalents
|
||||
|
||||
|
||||
class SDG123Metrics(BaseModel):
|
||||
"""UN SDG 12.3 specific metrics"""
|
||||
baseline_waste_percentage: float = Field(description="Baseline waste percentage")
|
||||
current_waste_percentage: float = Field(description="Current waste percentage")
|
||||
reduction_achieved: float = Field(description="Reduction achieved from baseline (%)")
|
||||
target_reduction: float = Field(description="Target reduction (50%)", default=50.0)
|
||||
progress_to_target: float = Field(description="Progress toward target (%)")
|
||||
status: str = Field(description="Status code: sdg_compliant, on_track, progressing, baseline")
|
||||
status_label: str = Field(description="Human-readable status")
|
||||
target_waste_percentage: float = Field(description="Target waste percentage to achieve")
|
||||
|
||||
|
||||
class SDGCompliance(BaseModel):
|
||||
"""SDG compliance assessment"""
|
||||
sdg_12_3: SDG123Metrics
|
||||
baseline_period: str = Field(description="Period used for baseline calculation")
|
||||
certification_ready: bool = Field(description="Ready for SDG certification")
|
||||
improvement_areas: List[str] = Field(description="Identified areas for improvement")
|
||||
|
||||
|
||||
class EnvironmentalImpactAvoided(BaseModel):
|
||||
"""Environmental impact avoided through AI"""
|
||||
co2_kg: float = Field(description="CO2 emissions avoided (kg)")
|
||||
water_liters: float = Field(description="Water saved (liters)")
|
||||
|
||||
|
||||
class AvoidedWaste(BaseModel):
|
||||
"""Waste avoided through AI predictions"""
|
||||
waste_avoided_kg: float = Field(description="Waste avoided in kilograms")
|
||||
ai_assisted_batches: int = Field(description="Number of AI-assisted batches")
|
||||
environmental_impact_avoided: EnvironmentalImpactAvoided
|
||||
methodology: str = Field(description="Calculation methodology")
|
||||
|
||||
|
||||
class FinancialImpact(BaseModel):
|
||||
"""Financial impact of waste"""
|
||||
waste_cost_eur: float = Field(description="Cost of waste in euros")
|
||||
cost_per_kg: float = Field(description="Average cost per kg")
|
||||
potential_monthly_savings: float = Field(description="Potential monthly savings")
|
||||
annual_projection: float = Field(description="Annual cost projection")
|
||||
|
||||
|
||||
class GrantProgramEligibility(BaseModel):
|
||||
"""Eligibility for a specific grant program"""
|
||||
eligible: bool = Field(description="Whether eligible for this grant")
|
||||
confidence: str = Field(description="Confidence level: high, medium, low")
|
||||
requirements_met: bool = Field(description="Whether requirements are met")
|
||||
funding_eur: float = Field(description="Available funding in euros")
|
||||
deadline: str = Field(description="Application deadline")
|
||||
program_type: str = Field(description="Type: grant, loan, or certification")
|
||||
sector_specific: Optional[str] = Field(None, description="Sector if specific: bakery, retail, etc.")
|
||||
|
||||
|
||||
class SpainCompliance(BaseModel):
|
||||
"""Spain-specific legal compliance"""
|
||||
law_1_2025: bool = Field(description="Compliance with Spanish Law 1/2025 on food waste")
|
||||
circular_economy_strategy: bool = Field(description="Aligned with Spanish Circular Economy Strategy")
|
||||
|
||||
|
||||
class GrantReadiness(BaseModel):
|
||||
"""Grant application readiness assessment"""
|
||||
overall_readiness_percentage: float = Field(description="Overall readiness percentage")
|
||||
grant_programs: Dict[str, GrantProgramEligibility] = Field(description="Eligibility by program")
|
||||
recommended_applications: List[str] = Field(description="Recommended grant programs to apply for")
|
||||
spain_compliance: SpainCompliance = Field(description="Spain-specific compliance status")
|
||||
|
||||
|
||||
class SustainabilityMetrics(BaseModel):
|
||||
"""Complete sustainability metrics response"""
|
||||
period: PeriodInfo
|
||||
waste_metrics: WasteMetrics
|
||||
environmental_impact: EnvironmentalImpact
|
||||
sdg_compliance: SDGCompliance
|
||||
avoided_waste: AvoidedWaste
|
||||
financial_impact: FinancialImpact
|
||||
grant_readiness: GrantReadiness
|
||||
|
||||
|
||||
class BaselineComparison(BaseModel):
|
||||
"""Baseline comparison for grants"""
|
||||
baseline: float
|
||||
current: float
|
||||
improvement: float
|
||||
|
||||
|
||||
class SupportingData(BaseModel):
|
||||
"""Supporting data for grant applications"""
|
||||
baseline_comparison: BaselineComparison
|
||||
environmental_benefits: EnvironmentalImpact
|
||||
financial_benefits: FinancialImpact
|
||||
|
||||
|
||||
class Certifications(BaseModel):
|
||||
"""Certification status"""
|
||||
sdg_12_3_compliant: bool
|
||||
grant_programs_eligible: List[str]
|
||||
|
||||
|
||||
class ExecutiveSummary(BaseModel):
|
||||
"""Executive summary for grant reports"""
|
||||
total_waste_reduced_kg: float
|
||||
waste_reduction_percentage: float
|
||||
co2_emissions_avoided_kg: float
|
||||
financial_savings_eur: float
|
||||
sdg_compliance_status: str
|
||||
|
||||
|
||||
class ReportMetadata(BaseModel):
|
||||
"""Report metadata"""
|
||||
generated_at: str
|
||||
report_type: str
|
||||
period: PeriodInfo
|
||||
tenant_id: str
|
||||
|
||||
|
||||
class GrantReport(BaseModel):
|
||||
"""Complete grant application report"""
|
||||
report_metadata: ReportMetadata
|
||||
executive_summary: ExecutiveSummary
|
||||
detailed_metrics: SustainabilityMetrics
|
||||
certifications: Certifications
|
||||
supporting_data: SupportingData
|
||||
|
||||
|
||||
# Request schemas
|
||||
|
||||
class SustainabilityMetricsRequest(BaseModel):
|
||||
"""Request for sustainability metrics"""
|
||||
start_date: Optional[datetime] = Field(None, description="Start date for metrics")
|
||||
end_date: Optional[datetime] = Field(None, description="End date for metrics")
|
||||
|
||||
|
||||
class GrantReportRequest(BaseModel):
|
||||
"""Request for grant report export"""
|
||||
grant_type: str = Field("general", description="Type of grant: general, eu_horizon, farm_to_fork, etc.")
|
||||
start_date: Optional[datetime] = Field(None, description="Start date for report")
|
||||
end_date: Optional[datetime] = Field(None, description="End date for report")
|
||||
format: str = Field("json", description="Export format: json, pdf, csv")
|
||||
|
||||
|
||||
# Widget/Dashboard schemas
|
||||
|
||||
class SustainabilityWidgetData(BaseModel):
|
||||
"""Simplified data for dashboard widgets"""
|
||||
total_waste_kg: float
|
||||
waste_reduction_percentage: float
|
||||
co2_saved_kg: float
|
||||
water_saved_liters: float
|
||||
trees_equivalent: float
|
||||
sdg_status: str
|
||||
sdg_progress: float
|
||||
grant_programs_ready: int
|
||||
financial_savings_eur: float
|
||||
Reference in New Issue
Block a user