demo seed change 7
This commit is contained in:
@@ -388,6 +388,7 @@ async def clone_demo_data(
|
||||
quality_score=batch_data.get('quality_score'),
|
||||
waste_quantity=batch_data.get('waste_quantity'),
|
||||
defect_quantity=batch_data.get('defect_quantity'),
|
||||
waste_defect_type=batch_data.get('waste_defect_type'),
|
||||
equipment_used=batch_data.get('equipment_used'),
|
||||
staff_assigned=batch_data.get('staff_assigned'),
|
||||
station_id=batch_data.get('station_id'),
|
||||
@@ -395,6 +396,7 @@ async def clone_demo_data(
|
||||
forecast_id=batch_data.get('forecast_id'),
|
||||
is_rush_order=batch_data.get('is_rush_order', False),
|
||||
is_special_recipe=batch_data.get('is_special_recipe', False),
|
||||
is_ai_assisted=batch_data.get('is_ai_assisted', False),
|
||||
production_notes=batch_data.get('production_notes'),
|
||||
quality_notes=batch_data.get('quality_notes'),
|
||||
delay_reason=batch_data.get('delay_reason'),
|
||||
|
||||
293
services/production/app/api/sustainability.py
Normal file
293
services/production/app/api/sustainability.py
Normal file
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
Production Service - Sustainability API
|
||||
Exposes production-specific sustainability metrics following microservices principles
|
||||
Each service owns its domain data
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
import structlog
|
||||
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from app.services.production_service import ProductionService
|
||||
from shared.routing import RouteBuilder
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Create route builder for consistent URL structure
|
||||
route_builder = RouteBuilder('production')
|
||||
|
||||
router = APIRouter(tags=["production-sustainability"])
|
||||
|
||||
|
||||
def get_production_service(request: Request) -> ProductionService:
|
||||
"""Dependency injection for production service"""
|
||||
from app.core.database import database_manager
|
||||
from app.core.config import settings
|
||||
notification_service = getattr(request.app.state, 'notification_service', None)
|
||||
return ProductionService(database_manager, settings, notification_service)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/tenants/{tenant_id}/production/sustainability/waste-metrics",
|
||||
response_model=dict,
|
||||
summary="Get production waste metrics",
|
||||
description="""
|
||||
Returns production-specific waste metrics for sustainability tracking.
|
||||
|
||||
This endpoint is part of the microservices architecture where each service
|
||||
owns its domain data. Frontend aggregates data from multiple services.
|
||||
|
||||
Metrics include:
|
||||
- Total production waste from batches (waste_quantity + defect_quantity)
|
||||
- Production volumes (planned vs actual)
|
||||
- Waste breakdown by defect type
|
||||
- AI-assisted batch tracking
|
||||
"""
|
||||
)
|
||||
async def get_production_waste_metrics(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
start_date: Optional[datetime] = Query(None, description="Start date for metrics (default: 30 days ago)"),
|
||||
end_date: Optional[datetime] = Query(None, description="End date for metrics (default: now)"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
production_service: ProductionService = Depends(get_production_service)
|
||||
):
|
||||
"""
|
||||
Get production waste metrics for sustainability dashboard
|
||||
|
||||
Returns production-specific metrics that frontend will aggregate with
|
||||
inventory metrics for complete sustainability picture.
|
||||
"""
|
||||
try:
|
||||
# Set default dates
|
||||
if not end_date:
|
||||
end_date = datetime.now()
|
||||
if not start_date:
|
||||
start_date = end_date - timedelta(days=30)
|
||||
|
||||
# Get waste analytics from production service
|
||||
waste_data = await production_service.get_waste_analytics(
|
||||
tenant_id=tenant_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
# Enrich with metadata
|
||||
response = {
|
||||
**waste_data,
|
||||
"service": "production",
|
||||
"period": {
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
"days": (end_date - start_date).days
|
||||
},
|
||||
"metadata": {
|
||||
"data_source": "production_batches",
|
||||
"calculation_method": "SUM(waste_quantity + defect_quantity)",
|
||||
"filters_applied": {
|
||||
"status": ["COMPLETED", "QUALITY_CHECK"],
|
||||
"date_range": f"{start_date.date()} to {end_date.date()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Production waste metrics retrieved",
|
||||
tenant_id=str(tenant_id),
|
||||
total_waste_kg=waste_data.get('total_production_waste', 0),
|
||||
period_days=(end_date - start_date).days,
|
||||
user_id=current_user.get('user_id')
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error getting production waste metrics",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e)
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/tenants/{tenant_id}/production/sustainability/baseline",
|
||||
response_model=dict,
|
||||
summary="Get production baseline metrics",
|
||||
description="""
|
||||
Returns baseline production metrics from the first 90 days of operation.
|
||||
|
||||
Used by frontend to calculate SDG 12.3 compliance (waste reduction targets).
|
||||
If tenant has less than 90 days of data, returns industry average baseline.
|
||||
"""
|
||||
)
|
||||
async def get_production_baseline(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
production_service: ProductionService = Depends(get_production_service)
|
||||
):
|
||||
"""
|
||||
Get baseline production metrics for SDG compliance calculations
|
||||
|
||||
Frontend uses this to calculate:
|
||||
- Waste reduction percentage vs baseline
|
||||
- Progress toward SDG 12.3 targets
|
||||
- Grant eligibility based on improvement
|
||||
"""
|
||||
try:
|
||||
baseline_data = await production_service.get_baseline_metrics(tenant_id)
|
||||
|
||||
# Add metadata
|
||||
response = {
|
||||
**baseline_data,
|
||||
"service": "production",
|
||||
"metadata": {
|
||||
"baseline_period_days": 90,
|
||||
"calculation_method": "First 90 days of production data",
|
||||
"fallback": "Industry average (25%) if insufficient data"
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Production baseline metrics retrieved",
|
||||
tenant_id=str(tenant_id),
|
||||
has_baseline=baseline_data.get('has_baseline', False),
|
||||
baseline_waste_pct=baseline_data.get('waste_percentage'),
|
||||
user_id=current_user.get('user_id')
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error getting production baseline",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e)
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/tenants/{tenant_id}/production/sustainability/ai-impact",
|
||||
response_model=dict,
|
||||
summary="Get AI waste reduction impact",
|
||||
description="""
|
||||
Analyzes the impact of AI-assisted production on waste reduction.
|
||||
|
||||
Compares waste rates between:
|
||||
- AI-assisted batches (with is_ai_assisted=true)
|
||||
- Manual batches (is_ai_assisted=false)
|
||||
|
||||
Shows ROI of AI features for sustainability.
|
||||
"""
|
||||
)
|
||||
async def get_ai_waste_impact(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
start_date: Optional[datetime] = Query(None, description="Start date (default: 30 days ago)"),
|
||||
end_date: Optional[datetime] = Query(None, description="End date (default: now)"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
production_service: ProductionService = Depends(get_production_service)
|
||||
):
|
||||
"""
|
||||
Get AI impact on waste reduction
|
||||
|
||||
Frontend uses this to showcase:
|
||||
- Value proposition of AI features
|
||||
- Waste avoided through AI assistance
|
||||
- Financial ROI of AI investment
|
||||
"""
|
||||
try:
|
||||
# Set default dates
|
||||
if not end_date:
|
||||
end_date = datetime.now()
|
||||
if not start_date:
|
||||
start_date = end_date - timedelta(days=30)
|
||||
|
||||
# Get AI impact analytics (we'll implement this)
|
||||
ai_impact = await production_service.get_ai_waste_impact(
|
||||
tenant_id=tenant_id,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"AI waste impact retrieved",
|
||||
tenant_id=str(tenant_id),
|
||||
ai_waste_reduction_pct=ai_impact.get('waste_reduction_percentage'),
|
||||
user_id=current_user.get('user_id')
|
||||
)
|
||||
|
||||
return ai_impact
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error getting AI waste impact",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e)
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/tenants/{tenant_id}/production/sustainability/summary",
|
||||
response_model=dict,
|
||||
summary="Get production sustainability summary",
|
||||
description="""
|
||||
Quick summary endpoint combining all production sustainability metrics.
|
||||
|
||||
Useful for dashboard widgets that need overview data without multiple calls.
|
||||
"""
|
||||
)
|
||||
async def get_production_sustainability_summary(
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
days: int = Query(30, ge=7, le=365, description="Number of days to analyze"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
production_service: ProductionService = Depends(get_production_service)
|
||||
):
|
||||
"""
|
||||
Get comprehensive production sustainability summary
|
||||
|
||||
Combines waste metrics, baseline, and AI impact in one response.
|
||||
Optimized for dashboard widgets.
|
||||
"""
|
||||
try:
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
# Get all metrics in parallel (within service)
|
||||
waste_data = await production_service.get_waste_analytics(tenant_id, start_date, end_date)
|
||||
baseline_data = await production_service.get_baseline_metrics(tenant_id)
|
||||
|
||||
# Try to get AI impact (may not be available for all tenants)
|
||||
try:
|
||||
ai_impact = await production_service.get_ai_waste_impact(tenant_id, start_date, end_date)
|
||||
except:
|
||||
ai_impact = {"available": False}
|
||||
|
||||
summary = {
|
||||
"service": "production",
|
||||
"period_days": days,
|
||||
"waste_metrics": waste_data,
|
||||
"baseline": baseline_data,
|
||||
"ai_impact": ai_impact,
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Production sustainability summary retrieved",
|
||||
tenant_id=str(tenant_id),
|
||||
period_days=days,
|
||||
user_id=current_user.get('user_id')
|
||||
)
|
||||
|
||||
return summary
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error getting production sustainability summary",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e)
|
||||
)
|
||||
raise
|
||||
@@ -30,7 +30,8 @@ from app.api import (
|
||||
production_orders_operations, # Tenant deletion endpoints
|
||||
audit,
|
||||
ml_insights, # ML insights endpoint
|
||||
batch
|
||||
batch,
|
||||
sustainability # Sustainability metrics endpoints
|
||||
)
|
||||
from app.api.internal_alert_trigger import router as internal_alert_trigger_router
|
||||
|
||||
@@ -214,6 +215,7 @@ service.add_router(production_schedules.router)
|
||||
service.add_router(production_operations.router)
|
||||
service.add_router(production_dashboard.router)
|
||||
service.add_router(analytics.router)
|
||||
service.add_router(sustainability.router) # Sustainability metrics endpoints
|
||||
service.add_router(internal_demo.router, tags=["internal-demo"])
|
||||
service.add_router(ml_insights.router) # ML insights endpoint
|
||||
service.add_router(ml_insights.internal_router) # Internal ML insights endpoint for demo cloning
|
||||
|
||||
@@ -1858,6 +1858,124 @@ class ProductionService:
|
||||
)
|
||||
raise
|
||||
|
||||
async def get_ai_waste_impact(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
start_date: datetime,
|
||||
end_date: datetime
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get AI impact on waste reduction
|
||||
|
||||
Compares waste rates between AI-assisted and manual batches
|
||||
to demonstrate ROI of AI features for sustainability.
|
||||
"""
|
||||
try:
|
||||
async with self.database_manager.get_session() as session:
|
||||
from app.repositories.production_batch_repository import ProductionBatchRepository
|
||||
from sqlalchemy import text
|
||||
|
||||
batch_repo = ProductionBatchRepository(session)
|
||||
|
||||
# Query for AI vs manual batch comparison
|
||||
query = text("""
|
||||
SELECT
|
||||
-- AI-assisted batches
|
||||
COUNT(CASE WHEN is_ai_assisted = true THEN 1 END) as ai_batches,
|
||||
COALESCE(SUM(CASE WHEN is_ai_assisted = true THEN planned_quantity ELSE 0 END), 0) as ai_planned,
|
||||
COALESCE(SUM(CASE WHEN is_ai_assisted = true THEN actual_quantity ELSE 0 END), 0) as ai_actual,
|
||||
COALESCE(SUM(CASE WHEN is_ai_assisted = true THEN waste_quantity ELSE 0 END), 0) as ai_waste,
|
||||
COALESCE(SUM(CASE WHEN is_ai_assisted = true THEN defect_quantity ELSE 0 END), 0) as ai_defects,
|
||||
|
||||
-- Manual batches
|
||||
COUNT(CASE WHEN is_ai_assisted = false THEN 1 END) as manual_batches,
|
||||
COALESCE(SUM(CASE WHEN is_ai_assisted = false THEN planned_quantity ELSE 0 END), 0) as manual_planned,
|
||||
COALESCE(SUM(CASE WHEN is_ai_assisted = false THEN actual_quantity ELSE 0 END), 0) as manual_actual,
|
||||
COALESCE(SUM(CASE WHEN is_ai_assisted = false THEN waste_quantity ELSE 0 END), 0) as manual_waste,
|
||||
COALESCE(SUM(CASE WHEN is_ai_assisted = false THEN defect_quantity ELSE 0 END), 0) as manual_defects
|
||||
FROM production_batches
|
||||
WHERE tenant_id = :tenant_id
|
||||
AND created_at BETWEEN :start_date AND :end_date
|
||||
AND status IN ('COMPLETED', 'QUALITY_CHECK')
|
||||
""")
|
||||
|
||||
result = await session.execute(
|
||||
query,
|
||||
{
|
||||
'tenant_id': tenant_id,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date
|
||||
}
|
||||
)
|
||||
row = result.fetchone()
|
||||
|
||||
# Calculate waste percentages
|
||||
ai_total_waste = float(row.ai_waste or 0) + float(row.ai_defects or 0)
|
||||
manual_total_waste = float(row.manual_waste or 0) + float(row.manual_defects or 0)
|
||||
|
||||
ai_waste_pct = (ai_total_waste / float(row.ai_planned)) * 100 if row.ai_planned > 0 else 0
|
||||
manual_waste_pct = (manual_total_waste / float(row.manual_planned)) * 100 if row.manual_planned > 0 else 0
|
||||
|
||||
# Calculate reduction
|
||||
waste_reduction_pct = 0
|
||||
if manual_waste_pct > 0:
|
||||
waste_reduction_pct = ((manual_waste_pct - ai_waste_pct) / manual_waste_pct) * 100
|
||||
|
||||
# Calculate waste avoided
|
||||
if manual_waste_pct > 0 and row.ai_planned > 0:
|
||||
waste_avoided_kg = (float(row.ai_planned) * (manual_waste_pct / 100)) - ai_total_waste
|
||||
else:
|
||||
waste_avoided_kg = 0
|
||||
|
||||
# Financial impact (€3.50/kg average waste cost)
|
||||
waste_cost_avoided = waste_avoided_kg * 3.50
|
||||
|
||||
ai_impact_data = {
|
||||
'ai_batches': {
|
||||
'count': int(row.ai_batches or 0),
|
||||
'production_kg': float(row.ai_planned or 0),
|
||||
'waste_kg': ai_total_waste,
|
||||
'waste_percentage': round(ai_waste_pct, 2)
|
||||
},
|
||||
'manual_batches': {
|
||||
'count': int(row.manual_batches or 0),
|
||||
'production_kg': float(row.manual_planned or 0),
|
||||
'waste_kg': manual_total_waste,
|
||||
'waste_percentage': round(manual_waste_pct, 2)
|
||||
},
|
||||
'impact': {
|
||||
'waste_reduction_percentage': round(waste_reduction_pct, 1),
|
||||
'waste_avoided_kg': round(waste_avoided_kg, 2),
|
||||
'cost_savings_eur': round(waste_cost_avoided, 2),
|
||||
'annual_projection_eur': round(waste_cost_avoided * 12, 2)
|
||||
},
|
||||
'adoption': {
|
||||
'ai_adoption_rate': round((int(row.ai_batches or 0) / (int(row.ai_batches or 0) + int(row.manual_batches or 1))) * 100, 1),
|
||||
'recommendation': 'increase_ai_usage' if waste_reduction_pct > 10 else 'monitor'
|
||||
},
|
||||
'period': {
|
||||
'start_date': start_date.isoformat(),
|
||||
'end_date': end_date.isoformat()
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"AI waste impact calculated",
|
||||
tenant_id=str(tenant_id),
|
||||
waste_reduction_pct=waste_reduction_pct,
|
||||
waste_avoided_kg=waste_avoided_kg
|
||||
)
|
||||
|
||||
return ai_impact_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error calculating AI waste impact",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e)
|
||||
)
|
||||
raise
|
||||
|
||||
# ================================================================
|
||||
# NEW: ORCHESTRATOR INTEGRATION
|
||||
# ================================================================
|
||||
|
||||
Reference in New Issue
Block a user