Add more services
This commit is contained in:
715
services/inventory/app/services/dashboard_service.py
Normal file
715
services/inventory/app/services/dashboard_service.py
Normal file
@@ -0,0 +1,715 @@
|
||||
# ================================================================
|
||||
# services/inventory/app/services/dashboard_service.py
|
||||
# ================================================================
|
||||
"""
|
||||
Dashboard Service - Orchestrates data from multiple sources for dashboard views
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
import structlog
|
||||
|
||||
from shared.database.transactions import transactional
|
||||
|
||||
from app.core.config import settings
|
||||
from app.services.inventory_service import InventoryService
|
||||
from app.services.food_safety_service import FoodSafetyService
|
||||
from app.schemas.dashboard import (
|
||||
InventoryDashboardSummary,
|
||||
BusinessModelInsights,
|
||||
InventoryAnalytics,
|
||||
DashboardFilter,
|
||||
AlertsFilter,
|
||||
StockStatusSummary,
|
||||
AlertSummary,
|
||||
RecentActivity
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class DashboardService:
|
||||
"""Service for dashboard data aggregation and analytics"""
|
||||
|
||||
def __init__(self, inventory_service: InventoryService, food_safety_service: FoodSafetyService):
|
||||
self.inventory_service = inventory_service
|
||||
self.food_safety_service = food_safety_service
|
||||
|
||||
@transactional
|
||||
async def get_inventory_dashboard_summary(
|
||||
self,
|
||||
db,
|
||||
tenant_id: UUID,
|
||||
filters: Optional[DashboardFilter] = None
|
||||
) -> InventoryDashboardSummary:
|
||||
"""Get comprehensive inventory dashboard summary"""
|
||||
try:
|
||||
logger.info("Building dashboard summary", tenant_id=str(tenant_id))
|
||||
|
||||
# Get basic inventory metrics
|
||||
inventory_summary = await self.inventory_service.get_inventory_summary(tenant_id)
|
||||
|
||||
# Get food safety metrics
|
||||
food_safety_dashboard = await self.food_safety_service.get_food_safety_dashboard(db, tenant_id)
|
||||
|
||||
# Get business model insights
|
||||
business_model = await self._detect_business_model(db, tenant_id)
|
||||
|
||||
# Get category breakdown
|
||||
stock_by_category = await self._get_stock_by_category(db, tenant_id)
|
||||
|
||||
# Get alerts breakdown
|
||||
alerts_by_severity = await self._get_alerts_by_severity(db, tenant_id)
|
||||
|
||||
# Get movements breakdown
|
||||
movements_by_type = await self._get_movements_by_type(db, tenant_id)
|
||||
|
||||
# Get performance indicators
|
||||
performance_metrics = await self._calculate_performance_indicators(db, tenant_id)
|
||||
|
||||
# Get trending data
|
||||
stock_value_trend = await self._get_stock_value_trend(db, tenant_id, days=30)
|
||||
alert_trend = await self._get_alert_trend(db, tenant_id, days=30)
|
||||
|
||||
# Recent activity
|
||||
recent_activity = await self.get_recent_activity(db, tenant_id, limit=10)
|
||||
|
||||
return InventoryDashboardSummary(
|
||||
# Current inventory metrics
|
||||
total_ingredients=inventory_summary.total_ingredients,
|
||||
active_ingredients=inventory_summary.total_ingredients, # Assuming all are active
|
||||
total_stock_value=inventory_summary.total_stock_value,
|
||||
total_stock_items=await self._get_total_stock_items(db, tenant_id),
|
||||
|
||||
# Stock status breakdown
|
||||
in_stock_items=await self._get_in_stock_count(db, tenant_id),
|
||||
low_stock_items=inventory_summary.low_stock_alerts,
|
||||
out_of_stock_items=inventory_summary.out_of_stock_items,
|
||||
expired_items=inventory_summary.expired_items,
|
||||
expiring_soon_items=inventory_summary.expiring_soon_items,
|
||||
|
||||
# Food safety metrics
|
||||
food_safety_alerts_active=food_safety_dashboard.critical_alerts + food_safety_dashboard.high_risk_items,
|
||||
temperature_violations_today=food_safety_dashboard.temperature_violations_24h,
|
||||
compliance_issues=food_safety_dashboard.non_compliant_items + food_safety_dashboard.pending_review_items,
|
||||
certifications_expiring_soon=food_safety_dashboard.certifications_expiring_soon,
|
||||
|
||||
# Recent activity
|
||||
recent_stock_movements=inventory_summary.recent_movements,
|
||||
recent_purchases=inventory_summary.recent_purchases,
|
||||
recent_waste=inventory_summary.recent_waste,
|
||||
recent_adjustments=0, # Would need to calculate
|
||||
|
||||
# Business model context
|
||||
business_model=business_model.get("model"),
|
||||
business_model_confidence=business_model.get("confidence"),
|
||||
|
||||
# Category breakdown
|
||||
stock_by_category=stock_by_category,
|
||||
alerts_by_severity=alerts_by_severity,
|
||||
movements_by_type=movements_by_type,
|
||||
|
||||
# Performance indicators
|
||||
inventory_turnover_ratio=performance_metrics.get("turnover_ratio"),
|
||||
waste_percentage=performance_metrics.get("waste_percentage"),
|
||||
compliance_score=performance_metrics.get("compliance_score"),
|
||||
cost_per_unit_avg=performance_metrics.get("avg_cost_per_unit"),
|
||||
|
||||
# Trending data
|
||||
stock_value_trend=stock_value_trend,
|
||||
alert_trend=alert_trend
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to build dashboard summary", error=str(e))
|
||||
raise
|
||||
|
||||
async def get_business_model_insights(
|
||||
self,
|
||||
db,
|
||||
tenant_id: UUID
|
||||
) -> BusinessModelInsights:
|
||||
"""Get business model insights based on inventory patterns"""
|
||||
try:
|
||||
# Get ingredient metrics
|
||||
ingredient_metrics = await self._get_ingredient_metrics(db, tenant_id)
|
||||
|
||||
# Get operational patterns
|
||||
operational_patterns = await self._analyze_operational_patterns(db, tenant_id)
|
||||
|
||||
# Detect business model
|
||||
model_detection = await self._detect_business_model(db, tenant_id)
|
||||
|
||||
# Generate recommendations
|
||||
recommendations = await self._generate_model_recommendations(
|
||||
model_detection["model"],
|
||||
ingredient_metrics,
|
||||
operational_patterns
|
||||
)
|
||||
|
||||
return BusinessModelInsights(
|
||||
detected_model=model_detection["model"],
|
||||
confidence_score=model_detection["confidence"],
|
||||
total_ingredient_types=ingredient_metrics["total_types"],
|
||||
average_stock_per_ingredient=ingredient_metrics["avg_stock"],
|
||||
finished_product_ratio=ingredient_metrics["finished_product_ratio"],
|
||||
supplier_diversity=ingredient_metrics["supplier_count"],
|
||||
order_frequency_pattern=operational_patterns["order_frequency"],
|
||||
seasonal_variation=operational_patterns["seasonal_variation"],
|
||||
bulk_purchasing_indicator=operational_patterns["bulk_indicator"],
|
||||
production_scale_indicator=operational_patterns["scale_indicator"],
|
||||
model_specific_recommendations=recommendations["specific"],
|
||||
optimization_opportunities=recommendations["optimization"]
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get business model insights", error=str(e))
|
||||
raise
|
||||
|
||||
async def get_inventory_analytics(
|
||||
self,
|
||||
db,
|
||||
tenant_id: UUID,
|
||||
days_back: int = 30
|
||||
) -> InventoryAnalytics:
|
||||
"""Get advanced inventory analytics"""
|
||||
try:
|
||||
# Get turnover analysis
|
||||
turnover_data = await self._analyze_inventory_turnover(db, tenant_id, days_back)
|
||||
|
||||
# Get cost analysis
|
||||
cost_analysis = await self._analyze_costs(db, tenant_id, days_back)
|
||||
|
||||
# Get efficiency metrics
|
||||
efficiency_metrics = await self._calculate_efficiency_metrics(db, tenant_id, days_back)
|
||||
|
||||
# Get quality and safety metrics
|
||||
quality_metrics = await self._calculate_quality_metrics(db, tenant_id, days_back)
|
||||
|
||||
# Get supplier performance
|
||||
supplier_performance = await self._analyze_supplier_performance(db, tenant_id, days_back)
|
||||
|
||||
return InventoryAnalytics(
|
||||
inventory_turnover_rate=turnover_data["turnover_rate"],
|
||||
fast_moving_items=turnover_data["fast_moving"],
|
||||
slow_moving_items=turnover_data["slow_moving"],
|
||||
dead_stock_items=turnover_data["dead_stock"],
|
||||
total_inventory_cost=cost_analysis["total_cost"],
|
||||
cost_by_category=cost_analysis["by_category"],
|
||||
average_unit_cost_trend=cost_analysis["cost_trend"],
|
||||
waste_cost_analysis=cost_analysis["waste_analysis"],
|
||||
stockout_frequency=efficiency_metrics["stockouts"],
|
||||
overstock_frequency=efficiency_metrics["overstocks"],
|
||||
reorder_accuracy=efficiency_metrics["reorder_accuracy"],
|
||||
forecast_accuracy=efficiency_metrics["forecast_accuracy"],
|
||||
quality_incidents_rate=quality_metrics["incidents_rate"],
|
||||
food_safety_score=quality_metrics["safety_score"],
|
||||
compliance_score_by_standard=quality_metrics["compliance_scores"],
|
||||
temperature_compliance_rate=quality_metrics["temperature_compliance"],
|
||||
supplier_performance=supplier_performance["performance"],
|
||||
delivery_reliability=supplier_performance["delivery_reliability"],
|
||||
quality_consistency=supplier_performance["quality_consistency"]
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get inventory analytics", error=str(e))
|
||||
raise
|
||||
|
||||
async def get_stock_status_by_category(
|
||||
self,
|
||||
db,
|
||||
tenant_id: UUID
|
||||
) -> List[StockStatusSummary]:
|
||||
"""Get stock status breakdown by category"""
|
||||
try:
|
||||
query = """
|
||||
SELECT
|
||||
COALESCE(i.ingredient_category::text, i.product_category::text, 'other') as category,
|
||||
COUNT(DISTINCT i.id) as total_ingredients,
|
||||
COUNT(CASE WHEN s.available_quantity > i.low_stock_threshold THEN 1 END) as in_stock,
|
||||
COUNT(CASE WHEN s.available_quantity <= i.low_stock_threshold AND s.available_quantity > 0 THEN 1 END) as low_stock,
|
||||
COUNT(CASE WHEN COALESCE(s.available_quantity, 0) = 0 THEN 1 END) as out_of_stock,
|
||||
COALESCE(SUM(s.available_quantity * s.unit_cost), 0) as total_value
|
||||
FROM ingredients i
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ingredient_id,
|
||||
SUM(available_quantity) as available_quantity,
|
||||
AVG(unit_cost) as unit_cost
|
||||
FROM stock
|
||||
WHERE tenant_id = :tenant_id AND is_available = true
|
||||
GROUP BY ingredient_id
|
||||
) s ON i.id = s.ingredient_id
|
||||
WHERE i.tenant_id = :tenant_id AND i.is_active = true
|
||||
GROUP BY category
|
||||
ORDER BY total_value DESC
|
||||
"""
|
||||
|
||||
result = await db.execute(query, {"tenant_id": tenant_id})
|
||||
rows = result.fetchall()
|
||||
|
||||
summaries = []
|
||||
total_value = sum(row.total_value for row in rows)
|
||||
|
||||
for row in rows:
|
||||
percentage = (row.total_value / total_value * 100) if total_value > 0 else 0
|
||||
|
||||
summaries.append(StockStatusSummary(
|
||||
category=row.category,
|
||||
total_ingredients=row.total_ingredients,
|
||||
in_stock=row.in_stock,
|
||||
low_stock=row.low_stock,
|
||||
out_of_stock=row.out_of_stock,
|
||||
total_value=Decimal(str(row.total_value)),
|
||||
percentage_of_total=Decimal(str(percentage))
|
||||
))
|
||||
|
||||
return summaries
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get stock status by category", error=str(e))
|
||||
raise
|
||||
|
||||
async def get_alerts_summary(
|
||||
self,
|
||||
db,
|
||||
tenant_id: UUID,
|
||||
filters: Optional[AlertsFilter] = None
|
||||
) -> List[AlertSummary]:
|
||||
"""Get alerts summary by type and severity"""
|
||||
try:
|
||||
# Build query with filters
|
||||
where_conditions = ["tenant_id = :tenant_id", "status = 'active'"]
|
||||
params = {"tenant_id": tenant_id}
|
||||
|
||||
if filters:
|
||||
if filters.alert_types:
|
||||
where_conditions.append("alert_type = ANY(:alert_types)")
|
||||
params["alert_types"] = filters.alert_types
|
||||
|
||||
if filters.severities:
|
||||
where_conditions.append("severity = ANY(:severities)")
|
||||
params["severities"] = filters.severities
|
||||
|
||||
if filters.date_from:
|
||||
where_conditions.append("created_at >= :date_from")
|
||||
params["date_from"] = filters.date_from
|
||||
|
||||
if filters.date_to:
|
||||
where_conditions.append("created_at <= :date_to")
|
||||
params["date_to"] = filters.date_to
|
||||
|
||||
where_clause = " AND ".join(where_conditions)
|
||||
|
||||
query = f"""
|
||||
SELECT
|
||||
alert_type,
|
||||
severity,
|
||||
COUNT(*) as count,
|
||||
MIN(EXTRACT(EPOCH FROM (NOW() - created_at))/3600)::int as oldest_alert_age_hours,
|
||||
AVG(CASE WHEN resolved_at IS NOT NULL
|
||||
THEN EXTRACT(EPOCH FROM (resolved_at - created_at))/3600
|
||||
ELSE NULL END)::int as avg_resolution_hours
|
||||
FROM food_safety_alerts
|
||||
WHERE {where_clause}
|
||||
GROUP BY alert_type, severity
|
||||
ORDER BY severity DESC, count DESC
|
||||
"""
|
||||
|
||||
result = await db.execute(query, params)
|
||||
rows = result.fetchall()
|
||||
|
||||
return [
|
||||
AlertSummary(
|
||||
alert_type=row.alert_type,
|
||||
severity=row.severity,
|
||||
count=row.count,
|
||||
oldest_alert_age_hours=row.oldest_alert_age_hours,
|
||||
average_resolution_time_hours=row.avg_resolution_hours
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get alerts summary", error=str(e))
|
||||
raise
|
||||
|
||||
async def get_recent_activity(
|
||||
self,
|
||||
db,
|
||||
tenant_id: UUID,
|
||||
limit: int = 20,
|
||||
activity_types: Optional[List[str]] = None
|
||||
) -> List[RecentActivity]:
|
||||
"""Get recent inventory activity"""
|
||||
try:
|
||||
activities = []
|
||||
|
||||
# Get recent stock movements
|
||||
stock_query = """
|
||||
SELECT
|
||||
'stock_movement' as activity_type,
|
||||
CASE
|
||||
WHEN movement_type = 'purchase' THEN 'Stock added: ' || i.name || ' (' || sm.quantity || ' ' || i.unit_of_measure::text || ')'
|
||||
WHEN movement_type = 'production_use' THEN 'Stock consumed: ' || i.name || ' (' || sm.quantity || ' ' || i.unit_of_measure::text || ')'
|
||||
WHEN movement_type = 'waste' THEN 'Stock wasted: ' || i.name || ' (' || sm.quantity || ' ' || i.unit_of_measure::text || ')'
|
||||
WHEN movement_type = 'adjustment' THEN 'Stock adjusted: ' || i.name || ' (' || sm.quantity || ' ' || i.unit_of_measure::text || ')'
|
||||
ELSE 'Stock movement: ' || i.name
|
||||
END as description,
|
||||
sm.movement_date as timestamp,
|
||||
sm.created_by as user_id,
|
||||
CASE
|
||||
WHEN movement_type = 'waste' THEN 'high'
|
||||
WHEN movement_type = 'adjustment' THEN 'medium'
|
||||
ELSE 'low'
|
||||
END as impact_level,
|
||||
sm.id as entity_id,
|
||||
'stock_movement' as entity_type
|
||||
FROM stock_movements sm
|
||||
JOIN ingredients i ON sm.ingredient_id = i.id
|
||||
WHERE i.tenant_id = :tenant_id
|
||||
ORDER BY sm.movement_date DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
|
||||
result = await db.execute(stock_query, {"tenant_id": tenant_id, "limit": limit // 2})
|
||||
for row in result.fetchall():
|
||||
activities.append(RecentActivity(
|
||||
activity_type=row.activity_type,
|
||||
description=row.description,
|
||||
timestamp=row.timestamp,
|
||||
impact_level=row.impact_level,
|
||||
entity_id=row.entity_id,
|
||||
entity_type=row.entity_type
|
||||
))
|
||||
|
||||
# Get recent food safety alerts
|
||||
alert_query = """
|
||||
SELECT
|
||||
'food_safety_alert' as activity_type,
|
||||
title as description,
|
||||
created_at as timestamp,
|
||||
created_by as user_id,
|
||||
CASE
|
||||
WHEN severity = 'critical' THEN 'high'
|
||||
WHEN severity = 'high' THEN 'medium'
|
||||
ELSE 'low'
|
||||
END as impact_level,
|
||||
id as entity_id,
|
||||
'food_safety_alert' as entity_type
|
||||
FROM food_safety_alerts
|
||||
WHERE tenant_id = :tenant_id
|
||||
ORDER BY created_at DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
|
||||
result = await db.execute(alert_query, {"tenant_id": tenant_id, "limit": limit // 2})
|
||||
for row in result.fetchall():
|
||||
activities.append(RecentActivity(
|
||||
activity_type=row.activity_type,
|
||||
description=row.description,
|
||||
timestamp=row.timestamp,
|
||||
impact_level=row.impact_level,
|
||||
entity_id=row.entity_id,
|
||||
entity_type=row.entity_type
|
||||
))
|
||||
|
||||
# Sort by timestamp and limit
|
||||
activities.sort(key=lambda x: x.timestamp, reverse=True)
|
||||
return activities[:limit]
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get recent activity", error=str(e))
|
||||
raise
|
||||
|
||||
async def get_live_metrics(self, db, tenant_id: UUID) -> Dict[str, Any]:
|
||||
"""Get real-time inventory metrics"""
|
||||
try:
|
||||
query = """
|
||||
SELECT
|
||||
COUNT(DISTINCT i.id) as total_ingredients,
|
||||
COUNT(CASE WHEN s.available_quantity > i.low_stock_threshold THEN 1 END) as in_stock,
|
||||
COUNT(CASE WHEN s.available_quantity <= i.low_stock_threshold THEN 1 END) as low_stock,
|
||||
COUNT(CASE WHEN s.available_quantity = 0 THEN 1 END) as out_of_stock,
|
||||
COALESCE(SUM(s.available_quantity * s.unit_cost), 0) as total_value,
|
||||
COUNT(CASE WHEN s.expiration_date < NOW() THEN 1 END) as expired_items,
|
||||
COUNT(CASE WHEN s.expiration_date BETWEEN NOW() AND NOW() + INTERVAL '7 days' THEN 1 END) as expiring_soon
|
||||
FROM ingredients i
|
||||
LEFT JOIN stock s ON i.id = s.ingredient_id AND s.is_available = true
|
||||
WHERE i.tenant_id = :tenant_id AND i.is_active = true
|
||||
"""
|
||||
|
||||
result = await db.execute(query, {"tenant_id": tenant_id})
|
||||
metrics = result.fetchone()
|
||||
|
||||
return {
|
||||
"total_ingredients": metrics.total_ingredients,
|
||||
"in_stock": metrics.in_stock,
|
||||
"low_stock": metrics.low_stock,
|
||||
"out_of_stock": metrics.out_of_stock,
|
||||
"total_value": float(metrics.total_value),
|
||||
"expired_items": metrics.expired_items,
|
||||
"expiring_soon": metrics.expiring_soon,
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get live metrics", error=str(e))
|
||||
raise
|
||||
|
||||
async def export_dashboard_data(
|
||||
self,
|
||||
db,
|
||||
tenant_id: UUID,
|
||||
format: str,
|
||||
date_from: Optional[datetime] = None,
|
||||
date_to: Optional[datetime] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Export dashboard data in specified format"""
|
||||
try:
|
||||
# Get dashboard summary
|
||||
summary = await self.get_inventory_dashboard_summary(db, tenant_id)
|
||||
|
||||
# Get analytics
|
||||
analytics = await self.get_inventory_analytics(db, tenant_id)
|
||||
|
||||
export_data = {
|
||||
"export_info": {
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"tenant_id": str(tenant_id),
|
||||
"format": format,
|
||||
"date_range": {
|
||||
"from": date_from.isoformat() if date_from else None,
|
||||
"to": date_to.isoformat() if date_to else None
|
||||
}
|
||||
},
|
||||
"dashboard_summary": summary.dict(),
|
||||
"analytics": analytics.dict()
|
||||
}
|
||||
|
||||
if format.lower() == "json":
|
||||
return export_data
|
||||
elif format.lower() in ["csv", "excel"]:
|
||||
# For CSV/Excel, flatten the data structure
|
||||
return {
|
||||
"message": f"Export in {format} format would be generated here",
|
||||
"data_preview": export_data
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Unsupported export format: {format}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to export dashboard data", error=str(e))
|
||||
raise
|
||||
|
||||
# ===== PRIVATE HELPER METHODS =====
|
||||
|
||||
async def _detect_business_model(self, db, tenant_id: UUID) -> Dict[str, Any]:
|
||||
"""Detect business model based on inventory patterns"""
|
||||
try:
|
||||
if not settings.ENABLE_BUSINESS_MODEL_DETECTION:
|
||||
return {"model": "unknown", "confidence": Decimal("0")}
|
||||
|
||||
# Get ingredient metrics
|
||||
query = """
|
||||
SELECT
|
||||
COUNT(*) as total_ingredients,
|
||||
COUNT(CASE WHEN product_type = 'finished_product' THEN 1 END) as finished_products,
|
||||
COUNT(CASE WHEN product_type = 'ingredient' THEN 1 END) as raw_ingredients,
|
||||
COUNT(DISTINCT supplier_name) as supplier_count,
|
||||
AVG(CASE WHEN s.available_quantity IS NOT NULL THEN s.available_quantity ELSE 0 END) as avg_stock_level
|
||||
FROM ingredients i
|
||||
LEFT JOIN (
|
||||
SELECT ingredient_id, SUM(available_quantity) as available_quantity
|
||||
FROM stock WHERE tenant_id = :tenant_id GROUP BY ingredient_id
|
||||
) s ON i.id = s.ingredient_id
|
||||
WHERE i.tenant_id = :tenant_id AND i.is_active = true
|
||||
"""
|
||||
|
||||
result = await db.execute(query, {"tenant_id": tenant_id})
|
||||
metrics = result.fetchone()
|
||||
|
||||
# Business model detection logic
|
||||
total_ingredients = metrics.total_ingredients
|
||||
finished_ratio = metrics.finished_products / total_ingredients if total_ingredients > 0 else 0
|
||||
|
||||
if total_ingredients >= settings.CENTRAL_BAKERY_THRESHOLD_INGREDIENTS:
|
||||
if finished_ratio > 0.3: # More than 30% finished products
|
||||
model = "central_bakery"
|
||||
confidence = Decimal("85")
|
||||
else:
|
||||
model = "central_bakery"
|
||||
confidence = Decimal("70")
|
||||
elif total_ingredients <= settings.INDIVIDUAL_BAKERY_THRESHOLD_INGREDIENTS:
|
||||
model = "individual_bakery"
|
||||
confidence = Decimal("80")
|
||||
else:
|
||||
model = "mixed"
|
||||
confidence = Decimal("60")
|
||||
|
||||
return {"model": model, "confidence": confidence}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to detect business model", error=str(e))
|
||||
return {"model": "unknown", "confidence": Decimal("0")}
|
||||
|
||||
async def _get_stock_by_category(self, db, tenant_id: UUID) -> Dict[str, Any]:
|
||||
"""Get stock breakdown by category"""
|
||||
try:
|
||||
query = """
|
||||
SELECT
|
||||
COALESCE(i.ingredient_category::text, i.product_category::text, 'other') as category,
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(s.available_quantity * s.unit_cost), 0) as total_value
|
||||
FROM ingredients i
|
||||
LEFT JOIN (
|
||||
SELECT ingredient_id, SUM(available_quantity) as available_quantity, AVG(unit_cost) as unit_cost
|
||||
FROM stock WHERE tenant_id = :tenant_id GROUP BY ingredient_id
|
||||
) s ON i.id = s.ingredient_id
|
||||
WHERE i.tenant_id = :tenant_id AND i.is_active = true
|
||||
GROUP BY category
|
||||
"""
|
||||
|
||||
result = await db.execute(query, {"tenant_id": tenant_id})
|
||||
categories = {}
|
||||
|
||||
for row in result.fetchall():
|
||||
categories[row.category] = {
|
||||
"count": row.count,
|
||||
"total_value": float(row.total_value)
|
||||
}
|
||||
|
||||
return categories
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get stock by category", error=str(e))
|
||||
return {}
|
||||
|
||||
async def _get_alerts_by_severity(self, db, tenant_id: UUID) -> Dict[str, int]:
|
||||
"""Get alerts breakdown by severity"""
|
||||
try:
|
||||
query = """
|
||||
SELECT severity, COUNT(*) as count
|
||||
FROM food_safety_alerts
|
||||
WHERE tenant_id = :tenant_id AND status = 'active'
|
||||
GROUP BY severity
|
||||
"""
|
||||
|
||||
result = await db.execute(query, {"tenant_id": tenant_id})
|
||||
alerts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
|
||||
|
||||
for row in result.fetchall():
|
||||
alerts[row.severity] = row.count
|
||||
|
||||
return alerts
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get alerts by severity", error=str(e))
|
||||
return {"critical": 0, "high": 0, "medium": 0, "low": 0}
|
||||
|
||||
async def _get_movements_by_type(self, db, tenant_id: UUID) -> Dict[str, int]:
|
||||
"""Get movements breakdown by type"""
|
||||
try:
|
||||
query = """
|
||||
SELECT sm.movement_type, COUNT(*) as count
|
||||
FROM stock_movements sm
|
||||
JOIN ingredients i ON sm.ingredient_id = i.id
|
||||
WHERE i.tenant_id = :tenant_id
|
||||
AND sm.movement_date > NOW() - INTERVAL '7 days'
|
||||
GROUP BY sm.movement_type
|
||||
"""
|
||||
|
||||
result = await db.execute(query, {"tenant_id": tenant_id})
|
||||
movements = {}
|
||||
|
||||
for row in result.fetchall():
|
||||
movements[row.movement_type] = row.count
|
||||
|
||||
return movements
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get movements by type", error=str(e))
|
||||
return {}
|
||||
|
||||
async def _calculate_performance_indicators(self, db, tenant_id: UUID) -> Dict[str, Decimal]:
|
||||
"""Calculate performance indicators"""
|
||||
try:
|
||||
# This would involve complex calculations
|
||||
# For now, return placeholder values
|
||||
return {
|
||||
"turnover_ratio": Decimal("4.2"),
|
||||
"waste_percentage": Decimal("2.1"),
|
||||
"compliance_score": Decimal("8.5"),
|
||||
"avg_cost_per_unit": Decimal("12.45")
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to calculate performance indicators", error=str(e))
|
||||
return {}
|
||||
|
||||
async def _get_stock_value_trend(self, db, tenant_id: UUID, days: int) -> List[Dict[str, Any]]:
|
||||
"""Get stock value trend over time"""
|
||||
try:
|
||||
# This would track stock value changes over time
|
||||
# For now, return sample data
|
||||
trend_data = []
|
||||
base_date = datetime.now() - timedelta(days=days)
|
||||
|
||||
for i in range(0, days, 7): # Weekly data points
|
||||
trend_data.append({
|
||||
"date": (base_date + timedelta(days=i)).isoformat(),
|
||||
"value": float(Decimal("50000") + Decimal(str(i * 100)))
|
||||
})
|
||||
|
||||
return trend_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get stock value trend", error=str(e))
|
||||
return []
|
||||
|
||||
async def _get_alert_trend(self, db, tenant_id: UUID, days: int) -> List[Dict[str, Any]]:
|
||||
"""Get alert trend over time"""
|
||||
try:
|
||||
query = """
|
||||
SELECT
|
||||
DATE(created_at) as alert_date,
|
||||
COUNT(*) as alert_count,
|
||||
COUNT(CASE WHEN severity IN ('high', 'critical') THEN 1 END) as high_severity_count
|
||||
FROM food_safety_alerts
|
||||
WHERE tenant_id = :tenant_id
|
||||
AND created_at > NOW() - INTERVAL '%s days'
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY alert_date
|
||||
""" % days
|
||||
|
||||
result = await db.execute(query, {"tenant_id": tenant_id})
|
||||
|
||||
return [
|
||||
{
|
||||
"date": row.alert_date.isoformat(),
|
||||
"total_alerts": row.alert_count,
|
||||
"high_severity_alerts": row.high_severity_count
|
||||
}
|
||||
for row in result.fetchall()
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get alert trend", error=str(e))
|
||||
return []
|
||||
|
||||
# Additional helper methods would be implemented here for:
|
||||
# - _get_total_stock_items
|
||||
# - _get_in_stock_count
|
||||
# - _get_ingredient_metrics
|
||||
# - _analyze_operational_patterns
|
||||
# - _generate_model_recommendations
|
||||
# - _analyze_inventory_turnover
|
||||
# - _analyze_costs
|
||||
# - _calculate_efficiency_metrics
|
||||
# - _calculate_quality_metrics
|
||||
# - _analyze_supplier_performance
|
||||
|
||||
# These are complex analytical methods that would require detailed implementation
|
||||
# based on specific business requirements and data structures
|
||||
Reference in New Issue
Block a user