721 lines
31 KiB
Python
721 lines
31 KiB
Python
# ================================================================
|
|
# services/suppliers/app/services/dashboard_service.py
|
|
# ================================================================
|
|
"""
|
|
Supplier Dashboard and Analytics Service
|
|
Comprehensive supplier performance dashboards and business intelligence
|
|
"""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import List, Optional, Dict, Any
|
|
from uuid import UUID
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, and_, or_, desc, asc, text
|
|
from decimal import Decimal
|
|
import structlog
|
|
|
|
from app.models.suppliers import (
|
|
Supplier, PurchaseOrder, Delivery, SupplierQualityReview,
|
|
SupplierStatus, SupplierType, PurchaseOrderStatus, DeliveryStatus
|
|
)
|
|
from app.models.performance import (
|
|
SupplierPerformanceMetric, SupplierScorecard, SupplierAlert,
|
|
PerformanceMetricType, PerformancePeriod, AlertSeverity, AlertStatus
|
|
)
|
|
from app.schemas.performance import (
|
|
PerformanceDashboardSummary, SupplierPerformanceInsights,
|
|
PerformanceAnalytics, BusinessModelInsights, AlertSummary
|
|
)
|
|
from app.core.config import settings
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
class DashboardService:
|
|
"""Service for supplier performance dashboards and analytics"""
|
|
|
|
def __init__(self):
|
|
self.logger = logger.bind(service="dashboard_service")
|
|
|
|
async def get_performance_dashboard_summary(
|
|
self,
|
|
db: AsyncSession,
|
|
tenant_id: UUID,
|
|
date_from: Optional[datetime] = None,
|
|
date_to: Optional[datetime] = None
|
|
) -> PerformanceDashboardSummary:
|
|
"""Get comprehensive performance dashboard summary"""
|
|
try:
|
|
# Default date range - last 30 days
|
|
if not date_to:
|
|
date_to = datetime.now(timezone.utc)
|
|
if not date_from:
|
|
date_from = date_to - timedelta(days=30)
|
|
|
|
self.logger.info("Generating dashboard summary",
|
|
tenant_id=str(tenant_id),
|
|
date_from=date_from.isoformat(),
|
|
date_to=date_to.isoformat())
|
|
|
|
# Get supplier statistics
|
|
supplier_stats = await self._get_supplier_statistics(db, tenant_id)
|
|
|
|
# Get performance statistics
|
|
performance_stats = await self._get_performance_statistics(db, tenant_id, date_from, date_to)
|
|
|
|
# Get alert statistics
|
|
alert_stats = await self._get_alert_statistics(db, tenant_id, date_from, date_to)
|
|
|
|
# Get financial statistics
|
|
financial_stats = await self._get_financial_statistics(db, tenant_id, date_from, date_to)
|
|
|
|
# Get business model insights
|
|
business_model = await self._detect_business_model(db, tenant_id)
|
|
|
|
# Calculate trends
|
|
trends = await self._calculate_performance_trends(db, tenant_id, date_from, date_to)
|
|
|
|
return PerformanceDashboardSummary(
|
|
total_suppliers=supplier_stats['total_suppliers'],
|
|
active_suppliers=supplier_stats['active_suppliers'],
|
|
suppliers_above_threshold=performance_stats['above_threshold'],
|
|
suppliers_below_threshold=performance_stats['below_threshold'],
|
|
average_overall_score=performance_stats['avg_overall_score'],
|
|
average_delivery_rate=performance_stats['avg_delivery_rate'],
|
|
average_quality_rate=performance_stats['avg_quality_rate'],
|
|
total_active_alerts=alert_stats['total_active'],
|
|
critical_alerts=alert_stats['critical_alerts'],
|
|
high_priority_alerts=alert_stats['high_priority'],
|
|
recent_scorecards_generated=performance_stats['recent_scorecards'],
|
|
cost_savings_this_month=financial_stats['cost_savings'],
|
|
performance_trend=trends['performance_trend'],
|
|
delivery_trend=trends['delivery_trend'],
|
|
quality_trend=trends['quality_trend'],
|
|
detected_business_model=business_model['model'],
|
|
model_confidence=business_model['confidence'],
|
|
business_model_metrics=business_model['metrics']
|
|
)
|
|
|
|
except Exception as e:
|
|
self.logger.error("Error generating dashboard summary", error=str(e))
|
|
raise
|
|
|
|
async def get_supplier_performance_insights(
|
|
self,
|
|
db: AsyncSession,
|
|
tenant_id: UUID,
|
|
supplier_id: UUID,
|
|
days_back: int = 30
|
|
) -> SupplierPerformanceInsights:
|
|
"""Get detailed performance insights for a specific supplier"""
|
|
try:
|
|
date_to = datetime.now(timezone.utc)
|
|
date_from = date_to - timedelta(days=days_back)
|
|
|
|
# Get supplier info
|
|
supplier = await self._get_supplier_info(db, supplier_id, tenant_id)
|
|
|
|
# Get current performance metrics
|
|
current_metrics = await self._get_current_performance_metrics(db, supplier_id, tenant_id)
|
|
|
|
# Get previous period metrics for comparison
|
|
previous_metrics = await self._get_previous_performance_metrics(db, supplier_id, tenant_id, days_back)
|
|
|
|
# Get recent activity statistics
|
|
activity_stats = await self._get_supplier_activity_stats(db, supplier_id, tenant_id, date_from, date_to)
|
|
|
|
# Get alert summary
|
|
alert_summary = await self._get_supplier_alert_summary(db, supplier_id, tenant_id, date_from, date_to)
|
|
|
|
# Calculate performance categorization
|
|
performance_category = self._categorize_performance(current_metrics.get('overall_score', 0))
|
|
risk_level = self._assess_risk_level(current_metrics, alert_summary)
|
|
|
|
# Generate recommendations
|
|
recommendations = await self._generate_supplier_recommendations(
|
|
db, supplier_id, tenant_id, current_metrics, activity_stats, alert_summary
|
|
)
|
|
|
|
return SupplierPerformanceInsights(
|
|
supplier_id=supplier_id,
|
|
current_overall_score=current_metrics.get('overall_score', 0),
|
|
previous_score=previous_metrics.get('overall_score'),
|
|
score_change_percentage=self._calculate_change_percentage(
|
|
current_metrics.get('overall_score', 0),
|
|
previous_metrics.get('overall_score')
|
|
),
|
|
performance_rank=current_metrics.get('rank'),
|
|
delivery_performance=current_metrics.get('delivery_performance', 0),
|
|
quality_performance=current_metrics.get('quality_performance', 0),
|
|
cost_performance=current_metrics.get('cost_performance', 0),
|
|
service_performance=current_metrics.get('service_performance', 0),
|
|
orders_last_30_days=activity_stats['orders_count'],
|
|
average_delivery_time=activity_stats['avg_delivery_time'],
|
|
quality_issues_count=activity_stats['quality_issues'],
|
|
cost_variance=activity_stats['cost_variance'],
|
|
active_alerts=alert_summary['active_count'],
|
|
resolved_alerts_last_30_days=alert_summary['resolved_count'],
|
|
alert_trend=alert_summary['trend'],
|
|
performance_category=performance_category,
|
|
risk_level=risk_level,
|
|
top_strengths=recommendations['strengths'],
|
|
improvement_priorities=recommendations['improvements'],
|
|
recommended_actions=recommendations['actions']
|
|
)
|
|
|
|
except Exception as e:
|
|
self.logger.error("Error generating supplier insights",
|
|
supplier_id=str(supplier_id),
|
|
error=str(e))
|
|
raise
|
|
|
|
async def get_performance_analytics(
|
|
self,
|
|
db: AsyncSession,
|
|
tenant_id: UUID,
|
|
period_days: int = 90
|
|
) -> PerformanceAnalytics:
|
|
"""Get advanced performance analytics"""
|
|
try:
|
|
date_to = datetime.now(timezone.utc)
|
|
date_from = date_to - timedelta(days=period_days)
|
|
|
|
# Get performance distribution
|
|
performance_distribution = await self._get_performance_distribution(db, tenant_id, date_from, date_to)
|
|
|
|
# Get trend analysis
|
|
trends = await self._get_detailed_trends(db, tenant_id, date_from, date_to)
|
|
|
|
# Get comparative analysis
|
|
comparative_analysis = await self._get_comparative_analysis(db, tenant_id, date_from, date_to)
|
|
|
|
# Get risk analysis
|
|
risk_analysis = await self._get_risk_analysis(db, tenant_id, date_from, date_to)
|
|
|
|
# Get financial impact
|
|
financial_impact = await self._get_financial_impact(db, tenant_id, date_from, date_to)
|
|
|
|
return PerformanceAnalytics(
|
|
period_start=date_from,
|
|
period_end=date_to,
|
|
total_suppliers_analyzed=performance_distribution['total_suppliers'],
|
|
performance_distribution=performance_distribution['distribution'],
|
|
score_ranges=performance_distribution['score_ranges'],
|
|
overall_trend=trends['overall'],
|
|
delivery_trends=trends['delivery'],
|
|
quality_trends=trends['quality'],
|
|
cost_trends=trends['cost'],
|
|
top_performers=comparative_analysis['top_performers'],
|
|
underperformers=comparative_analysis['underperformers'],
|
|
most_improved=comparative_analysis['most_improved'],
|
|
biggest_declines=comparative_analysis['biggest_declines'],
|
|
high_risk_suppliers=risk_analysis['high_risk'],
|
|
contract_renewals_due=risk_analysis['contract_renewals'],
|
|
certification_expiries=risk_analysis['certification_expiries'],
|
|
total_procurement_value=financial_impact['total_value'],
|
|
cost_savings_achieved=financial_impact['cost_savings'],
|
|
cost_avoidance=financial_impact['cost_avoidance'],
|
|
financial_risk_exposure=financial_impact['risk_exposure']
|
|
)
|
|
|
|
except Exception as e:
|
|
self.logger.error("Error generating performance analytics", error=str(e))
|
|
raise
|
|
|
|
async def get_business_model_insights(
|
|
self,
|
|
db: AsyncSession,
|
|
tenant_id: UUID
|
|
) -> BusinessModelInsights:
|
|
"""Get business model detection and insights"""
|
|
try:
|
|
# Analyze supplier patterns
|
|
supplier_patterns = await self._analyze_supplier_patterns(db, tenant_id)
|
|
|
|
# Detect business model
|
|
business_model = await self._detect_business_model_detailed(db, tenant_id)
|
|
|
|
# Generate optimization recommendations
|
|
optimization = await self._generate_optimization_recommendations(db, tenant_id, business_model)
|
|
|
|
# Get benchmarking data
|
|
benchmarking = await self._get_benchmarking_data(db, tenant_id, business_model['model'])
|
|
|
|
return BusinessModelInsights(
|
|
detected_model=business_model['model'],
|
|
confidence_score=business_model['confidence'],
|
|
model_characteristics=business_model['characteristics'],
|
|
supplier_diversity_score=supplier_patterns['diversity_score'],
|
|
procurement_volume_patterns=supplier_patterns['volume_patterns'],
|
|
delivery_frequency_patterns=supplier_patterns['delivery_patterns'],
|
|
order_size_patterns=supplier_patterns['order_size_patterns'],
|
|
optimization_opportunities=optimization['opportunities'],
|
|
recommended_supplier_mix=optimization['supplier_mix'],
|
|
cost_optimization_potential=optimization['cost_potential'],
|
|
risk_mitigation_suggestions=optimization['risk_mitigation'],
|
|
industry_comparison=benchmarking['industry'],
|
|
peer_comparison=benchmarking.get('peer')
|
|
)
|
|
|
|
except Exception as e:
|
|
self.logger.error("Error generating business model insights", error=str(e))
|
|
raise
|
|
|
|
async def get_alert_summary(
|
|
self,
|
|
db: AsyncSession,
|
|
tenant_id: UUID,
|
|
date_from: Optional[datetime] = None,
|
|
date_to: Optional[datetime] = None
|
|
) -> List[AlertSummary]:
|
|
"""Get alert summary by type and severity"""
|
|
try:
|
|
if not date_to:
|
|
date_to = datetime.now(timezone.utc)
|
|
if not date_from:
|
|
date_from = date_to - timedelta(days=30)
|
|
|
|
query = select(
|
|
SupplierAlert.alert_type,
|
|
SupplierAlert.severity,
|
|
func.count(SupplierAlert.id).label('count'),
|
|
func.avg(
|
|
func.extract('epoch', SupplierAlert.resolved_at - SupplierAlert.triggered_at) / 3600
|
|
).label('avg_resolution_hours'),
|
|
func.max(
|
|
func.extract('epoch', func.current_timestamp() - SupplierAlert.triggered_at) / 3600
|
|
).label('oldest_age_hours')
|
|
).where(
|
|
and_(
|
|
SupplierAlert.tenant_id == tenant_id,
|
|
SupplierAlert.triggered_at >= date_from,
|
|
SupplierAlert.triggered_at <= date_to
|
|
)
|
|
).group_by(SupplierAlert.alert_type, SupplierAlert.severity)
|
|
|
|
result = await db.execute(query)
|
|
rows = result.all()
|
|
|
|
alert_summaries = []
|
|
for row in rows:
|
|
alert_summaries.append(AlertSummary(
|
|
alert_type=row.alert_type,
|
|
severity=row.severity,
|
|
count=row.count,
|
|
avg_resolution_time_hours=row.avg_resolution_hours,
|
|
oldest_alert_age_hours=row.oldest_age_hours
|
|
))
|
|
|
|
return alert_summaries
|
|
|
|
except Exception as e:
|
|
self.logger.error("Error getting alert summary", error=str(e))
|
|
raise
|
|
|
|
# === Private Helper Methods ===
|
|
|
|
async def _get_supplier_statistics(self, db: AsyncSession, tenant_id: UUID) -> Dict[str, int]:
|
|
"""Get basic supplier statistics"""
|
|
query = select(
|
|
func.count(Supplier.id).label('total_suppliers'),
|
|
func.count(Supplier.id.filter(Supplier.status == SupplierStatus.ACTIVE)).label('active_suppliers')
|
|
).where(Supplier.tenant_id == tenant_id)
|
|
|
|
result = await db.execute(query)
|
|
row = result.first()
|
|
|
|
return {
|
|
'total_suppliers': row.total_suppliers or 0,
|
|
'active_suppliers': row.active_suppliers or 0
|
|
}
|
|
|
|
async def _get_performance_statistics(
|
|
self,
|
|
db: AsyncSession,
|
|
tenant_id: UUID,
|
|
date_from: datetime,
|
|
date_to: datetime
|
|
) -> Dict[str, Any]:
|
|
"""Get performance statistics"""
|
|
# Get recent performance metrics
|
|
query = select(
|
|
func.avg(SupplierPerformanceMetric.metric_value).label('avg_score'),
|
|
func.count(
|
|
SupplierPerformanceMetric.id.filter(
|
|
SupplierPerformanceMetric.metric_value >= settings.GOOD_DELIVERY_RATE
|
|
)
|
|
).label('above_threshold'),
|
|
func.count(
|
|
SupplierPerformanceMetric.id.filter(
|
|
SupplierPerformanceMetric.metric_value < settings.GOOD_DELIVERY_RATE
|
|
)
|
|
).label('below_threshold')
|
|
).where(
|
|
and_(
|
|
SupplierPerformanceMetric.tenant_id == tenant_id,
|
|
SupplierPerformanceMetric.calculated_at >= date_from,
|
|
SupplierPerformanceMetric.calculated_at <= date_to,
|
|
SupplierPerformanceMetric.metric_type == PerformanceMetricType.DELIVERY_PERFORMANCE
|
|
)
|
|
)
|
|
|
|
result = await db.execute(query)
|
|
row = result.first()
|
|
|
|
# Get quality statistics
|
|
quality_query = select(
|
|
func.avg(SupplierPerformanceMetric.metric_value).label('avg_quality')
|
|
).where(
|
|
and_(
|
|
SupplierPerformanceMetric.tenant_id == tenant_id,
|
|
SupplierPerformanceMetric.calculated_at >= date_from,
|
|
SupplierPerformanceMetric.calculated_at <= date_to,
|
|
SupplierPerformanceMetric.metric_type == PerformanceMetricType.QUALITY_SCORE
|
|
)
|
|
)
|
|
|
|
quality_result = await db.execute(quality_query)
|
|
quality_row = quality_result.first()
|
|
|
|
# Get scorecard count
|
|
scorecard_query = select(func.count(SupplierScorecard.id)).where(
|
|
and_(
|
|
SupplierScorecard.tenant_id == tenant_id,
|
|
SupplierScorecard.generated_at >= date_from,
|
|
SupplierScorecard.generated_at <= date_to
|
|
)
|
|
)
|
|
|
|
scorecard_result = await db.execute(scorecard_query)
|
|
scorecard_count = scorecard_result.scalar() or 0
|
|
|
|
return {
|
|
'avg_overall_score': row.avg_score or 0,
|
|
'above_threshold': row.above_threshold or 0,
|
|
'below_threshold': row.below_threshold or 0,
|
|
'avg_delivery_rate': row.avg_score or 0,
|
|
'avg_quality_rate': quality_row.avg_quality or 0,
|
|
'recent_scorecards': scorecard_count
|
|
}
|
|
|
|
async def _get_alert_statistics(
|
|
self,
|
|
db: AsyncSession,
|
|
tenant_id: UUID,
|
|
date_from: datetime,
|
|
date_to: datetime
|
|
) -> Dict[str, int]:
|
|
"""Get alert statistics"""
|
|
query = select(
|
|
func.count(SupplierAlert.id.filter(SupplierAlert.status == AlertStatus.ACTIVE)).label('total_active'),
|
|
func.count(SupplierAlert.id.filter(SupplierAlert.severity == AlertSeverity.CRITICAL)).label('critical'),
|
|
func.count(SupplierAlert.id.filter(SupplierAlert.priority_score >= 70)).label('high_priority')
|
|
).where(
|
|
and_(
|
|
SupplierAlert.tenant_id == tenant_id,
|
|
SupplierAlert.triggered_at >= date_from,
|
|
SupplierAlert.triggered_at <= date_to
|
|
)
|
|
)
|
|
|
|
result = await db.execute(query)
|
|
row = result.first()
|
|
|
|
return {
|
|
'total_active': row.total_active or 0,
|
|
'critical_alerts': row.critical or 0,
|
|
'high_priority': row.high_priority or 0
|
|
}
|
|
|
|
async def _get_financial_statistics(
|
|
self,
|
|
db: AsyncSession,
|
|
tenant_id: UUID,
|
|
date_from: datetime,
|
|
date_to: datetime
|
|
) -> Dict[str, Decimal]:
|
|
"""Get financial statistics"""
|
|
# Calculate potential cost savings based on supplier performance
|
|
# Cost savings estimated from quality issues avoided, on-time deliveries, etc.
|
|
|
|
# Get purchase orders in period
|
|
query = select(
|
|
func.sum(PurchaseOrder.total_amount).label('total_spent')
|
|
).where(
|
|
and_(
|
|
PurchaseOrder.tenant_id == tenant_id,
|
|
PurchaseOrder.created_at >= date_from,
|
|
PurchaseOrder.created_at <= date_to,
|
|
PurchaseOrder.status.in_([
|
|
PurchaseOrderStatus.RECEIVED,
|
|
PurchaseOrderStatus.PARTIALLY_RECEIVED,
|
|
PurchaseOrderStatus.COMPLETED
|
|
])
|
|
)
|
|
)
|
|
|
|
result = await db.execute(query)
|
|
row = result.first()
|
|
total_spent = row.total_spent or Decimal('0')
|
|
|
|
# Estimate cost savings as 2-5% of total spent based on:
|
|
# - Better supplier selection
|
|
# - Reduced waste from quality issues
|
|
# - Better pricing through supplier comparison
|
|
estimated_savings_percentage = Decimal('0.03') # 3% conservative estimate
|
|
cost_savings = total_spent * estimated_savings_percentage
|
|
|
|
return {
|
|
'cost_savings': cost_savings
|
|
}
|
|
|
|
async def _detect_business_model(self, db: AsyncSession, tenant_id: UUID) -> Dict[str, Any]:
|
|
"""Detect business model based on supplier patterns"""
|
|
# Get supplier count by category
|
|
query = select(
|
|
func.count(Supplier.id).label('total_suppliers'),
|
|
func.count(Supplier.id.filter(Supplier.supplier_type == SupplierType.INGREDIENTS)).label('ingredient_suppliers')
|
|
).where(
|
|
and_(
|
|
Supplier.tenant_id == tenant_id,
|
|
Supplier.status == SupplierStatus.ACTIVE
|
|
)
|
|
)
|
|
|
|
result = await db.execute(query)
|
|
row = result.first()
|
|
|
|
total_suppliers = row.total_suppliers or 0
|
|
ingredient_suppliers = row.ingredient_suppliers or 0
|
|
|
|
# Simple business model detection logic
|
|
if total_suppliers >= settings.CENTRAL_BAKERY_THRESHOLD_SUPPLIERS:
|
|
model = "central_bakery"
|
|
confidence = 0.85
|
|
elif total_suppliers >= settings.INDIVIDUAL_BAKERY_THRESHOLD_SUPPLIERS:
|
|
model = "individual_bakery"
|
|
confidence = 0.75
|
|
else:
|
|
model = "small_bakery"
|
|
confidence = 0.60
|
|
|
|
return {
|
|
'model': model,
|
|
'confidence': confidence,
|
|
'metrics': {
|
|
'total_suppliers': total_suppliers,
|
|
'ingredient_suppliers': ingredient_suppliers,
|
|
'supplier_diversity': ingredient_suppliers / max(total_suppliers, 1)
|
|
}
|
|
}
|
|
|
|
async def _calculate_performance_trends(
|
|
self,
|
|
db: AsyncSession,
|
|
tenant_id: UUID,
|
|
date_from: datetime,
|
|
date_to: datetime
|
|
) -> Dict[str, str]:
|
|
"""Calculate performance trends based on historical data"""
|
|
|
|
# Calculate period length and compare with previous period
|
|
period_length = (date_to - date_from).days
|
|
previous_period_start = date_from - timedelta(days=period_length)
|
|
previous_period_end = date_from
|
|
|
|
# Get current period metrics
|
|
current_query = select(
|
|
func.avg(Supplier.delivery_rating).label('avg_delivery'),
|
|
func.avg(Supplier.quality_rating).label('avg_quality'),
|
|
func.count(PurchaseOrder.id).label('order_count')
|
|
).select_from(PurchaseOrder).join(
|
|
Supplier, PurchaseOrder.supplier_id == Supplier.id
|
|
).where(
|
|
and_(
|
|
PurchaseOrder.tenant_id == tenant_id,
|
|
PurchaseOrder.created_at >= date_from,
|
|
PurchaseOrder.created_at <= date_to
|
|
)
|
|
)
|
|
|
|
current_result = await db.execute(current_query)
|
|
current = current_result.first()
|
|
|
|
# Get previous period metrics
|
|
previous_query = select(
|
|
func.avg(Supplier.delivery_rating).label('avg_delivery'),
|
|
func.avg(Supplier.quality_rating).label('avg_quality'),
|
|
func.count(PurchaseOrder.id).label('order_count')
|
|
).select_from(PurchaseOrder).join(
|
|
Supplier, PurchaseOrder.supplier_id == Supplier.id
|
|
).where(
|
|
and_(
|
|
PurchaseOrder.tenant_id == tenant_id,
|
|
PurchaseOrder.created_at >= previous_period_start,
|
|
PurchaseOrder.created_at < previous_period_end
|
|
)
|
|
)
|
|
|
|
previous_result = await db.execute(previous_query)
|
|
previous = previous_result.first()
|
|
|
|
# Calculate trends
|
|
def calculate_trend(current_value, previous_value, threshold=0.05):
|
|
"""Calculate trend direction based on percentage change"""
|
|
if not current_value or not previous_value:
|
|
return 'stable'
|
|
change = (current_value - previous_value) / previous_value
|
|
if change > threshold:
|
|
return 'improving'
|
|
elif change < -threshold:
|
|
return 'declining'
|
|
return 'stable'
|
|
|
|
delivery_trend = calculate_trend(
|
|
current.avg_delivery if current else None,
|
|
previous.avg_delivery if previous else None
|
|
)
|
|
|
|
quality_trend = calculate_trend(
|
|
current.avg_quality if current else None,
|
|
previous.avg_quality if previous else None
|
|
)
|
|
|
|
# Overall performance based on both metrics
|
|
if delivery_trend == 'improving' and quality_trend == 'improving':
|
|
performance_trend = 'improving'
|
|
elif delivery_trend == 'declining' or quality_trend == 'declining':
|
|
performance_trend = 'declining'
|
|
else:
|
|
performance_trend = 'stable'
|
|
|
|
return {
|
|
'performance_trend': performance_trend,
|
|
'delivery_trend': delivery_trend,
|
|
'quality_trend': quality_trend
|
|
}
|
|
|
|
def _categorize_performance(self, score: float) -> str:
|
|
"""Categorize performance based on score"""
|
|
if score >= settings.EXCELLENT_DELIVERY_RATE:
|
|
return "excellent"
|
|
elif score >= settings.GOOD_DELIVERY_RATE:
|
|
return "good"
|
|
elif score >= settings.ACCEPTABLE_DELIVERY_RATE:
|
|
return "acceptable"
|
|
elif score >= settings.POOR_DELIVERY_RATE:
|
|
return "needs_improvement"
|
|
else:
|
|
return "poor"
|
|
|
|
def _assess_risk_level(self, metrics: Dict[str, Any], alerts: Dict[str, Any]) -> str:
|
|
"""Assess risk level based on metrics and alerts"""
|
|
if alerts.get('active_count', 0) > 3 or metrics.get('overall_score', 0) < 50:
|
|
return "critical"
|
|
elif alerts.get('active_count', 0) > 1 or metrics.get('overall_score', 0) < 70:
|
|
return "high"
|
|
elif alerts.get('active_count', 0) > 0 or metrics.get('overall_score', 0) < 85:
|
|
return "medium"
|
|
else:
|
|
return "low"
|
|
|
|
def _calculate_change_percentage(self, current: float, previous: Optional[float]) -> Optional[float]:
|
|
"""Calculate percentage change between current and previous values"""
|
|
if previous is None or previous == 0:
|
|
return None
|
|
return ((current - previous) / previous) * 100
|
|
|
|
# === Placeholder methods for complex analytics ===
|
|
# These methods return placeholder data and should be implemented with actual business logic
|
|
|
|
async def _get_supplier_info(self, db: AsyncSession, supplier_id: UUID, tenant_id: UUID) -> Dict[str, Any]:
|
|
stmt = select(Supplier).where(and_(Supplier.id == supplier_id, Supplier.tenant_id == tenant_id))
|
|
result = await db.execute(stmt)
|
|
supplier = result.scalar_one_or_none()
|
|
return {'name': supplier.name if supplier else 'Unknown Supplier'}
|
|
|
|
async def _get_current_performance_metrics(self, db: AsyncSession, supplier_id: UUID, tenant_id: UUID) -> Dict[str, Any]:
|
|
return {'overall_score': 75.0, 'delivery_performance': 80.0, 'quality_performance': 85.0, 'cost_performance': 70.0, 'service_performance': 75.0}
|
|
|
|
async def _get_previous_performance_metrics(self, db: AsyncSession, supplier_id: UUID, tenant_id: UUID, days_back: int) -> Dict[str, Any]:
|
|
return {'overall_score': 70.0}
|
|
|
|
async def _get_supplier_activity_stats(self, db: AsyncSession, supplier_id: UUID, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
|
return {'orders_count': 15, 'avg_delivery_time': 3.2, 'quality_issues': 2, 'cost_variance': 5.5}
|
|
|
|
async def _get_supplier_alert_summary(self, db: AsyncSession, supplier_id: UUID, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
|
return {'active_count': 1, 'resolved_count': 3, 'trend': 'improving'}
|
|
|
|
async def _generate_supplier_recommendations(self, db: AsyncSession, supplier_id: UUID, tenant_id: UUID, metrics: Dict[str, Any], activity: Dict[str, Any], alerts: Dict[str, Any]) -> Dict[str, Any]:
|
|
return {
|
|
'strengths': ['Consistent quality', 'Reliable delivery'],
|
|
'improvements': ['Cost optimization', 'Communication'],
|
|
'actions': [{'action': 'Negotiate better pricing', 'priority': 'high'}]
|
|
}
|
|
|
|
async def _get_performance_distribution(self, db: AsyncSession, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
|
return {
|
|
'total_suppliers': 25,
|
|
'distribution': {'excellent': 5, 'good': 12, 'acceptable': 6, 'poor': 2},
|
|
'score_ranges': {'excellent': [95, 100, 97.5], 'good': [80, 94, 87.0]}
|
|
}
|
|
|
|
async def _get_detailed_trends(self, db: AsyncSession, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
|
return {
|
|
'overall': {'month_over_month': 2.5},
|
|
'delivery': {'month_over_month': 1.8},
|
|
'quality': {'month_over_month': 3.2},
|
|
'cost': {'month_over_month': -1.5}
|
|
}
|
|
|
|
async def _get_comparative_analysis(self, db: AsyncSession, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
|
return {
|
|
'top_performers': [],
|
|
'underperformers': [],
|
|
'most_improved': [],
|
|
'biggest_declines': []
|
|
}
|
|
|
|
async def _get_risk_analysis(self, db: AsyncSession, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
|
return {
|
|
'high_risk': [],
|
|
'contract_renewals': [],
|
|
'certification_expiries': []
|
|
}
|
|
|
|
async def _get_financial_impact(self, db: AsyncSession, tenant_id: UUID, date_from: datetime, date_to: datetime) -> Dict[str, Any]:
|
|
return {
|
|
'total_value': Decimal('150000'),
|
|
'cost_savings': Decimal('5000'),
|
|
'cost_avoidance': Decimal('2000'),
|
|
'risk_exposure': Decimal('10000')
|
|
}
|
|
|
|
async def _analyze_supplier_patterns(self, db: AsyncSession, tenant_id: UUID) -> Dict[str, Any]:
|
|
return {
|
|
'diversity_score': 75.0,
|
|
'volume_patterns': {'peak_months': ['March', 'December']},
|
|
'delivery_patterns': {'frequency': 'weekly'},
|
|
'order_size_patterns': {'average_size': 'medium'}
|
|
}
|
|
|
|
async def _detect_business_model_detailed(self, db: AsyncSession, tenant_id: UUID) -> Dict[str, Any]:
|
|
return {
|
|
'model': 'individual_bakery',
|
|
'confidence': 0.85,
|
|
'characteristics': {'supplier_count': 15, 'order_frequency': 'weekly'}
|
|
}
|
|
|
|
async def _generate_optimization_recommendations(self, db: AsyncSession, tenant_id: UUID, business_model: Dict[str, Any]) -> Dict[str, Any]:
|
|
return {
|
|
'opportunities': [{'type': 'consolidation', 'potential_savings': '10%'}],
|
|
'supplier_mix': {'ingredients': '60%', 'packaging': '25%', 'services': '15%'},
|
|
'cost_potential': Decimal('5000'),
|
|
'risk_mitigation': ['Diversify supplier base', 'Implement backup suppliers']
|
|
}
|
|
|
|
async def _get_benchmarking_data(self, db: AsyncSession, tenant_id: UUID, business_model: str) -> Dict[str, Any]:
|
|
return {
|
|
'industry': {'delivery_rate': 88.5, 'quality_score': 91.2},
|
|
'peer': {'delivery_rate': 86.8, 'quality_score': 89.5}
|
|
} |