Improve te panel de control logic
This commit is contained in:
@@ -456,6 +456,9 @@ class ProductionServiceClient(BaseServiceClient):
|
||||
"""
|
||||
Get today's production batches for dashboard timeline
|
||||
|
||||
For demo compatibility: Queries all recent batches and filters for actionable ones
|
||||
scheduled for today, since demo session dates are adjusted relative to session creation time.
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
|
||||
@@ -463,13 +466,67 @@ class ProductionServiceClient(BaseServiceClient):
|
||||
Dict with ProductionBatchListResponse: {"batches": [...], "total_count": n, "page": 1, "page_size": n}
|
||||
"""
|
||||
try:
|
||||
from datetime import date
|
||||
today = date.today()
|
||||
return await self.get(
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
# Get today's date range (start of day to end of day in UTC)
|
||||
now = datetime.now(timezone.utc)
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_end = today_start + timedelta(days=1)
|
||||
|
||||
# Query all batches without date/status filter for demo compatibility
|
||||
# The dashboard will filter for PENDING, IN_PROGRESS, or SCHEDULED
|
||||
result = await self.get(
|
||||
"/production/batches",
|
||||
tenant_id=tenant_id,
|
||||
params={"start_date": today.isoformat(), "end_date": today.isoformat(), "page_size": 100}
|
||||
params={"page_size": 100}
|
||||
)
|
||||
|
||||
if result and "batches" in result:
|
||||
# Filter for actionable batches scheduled for TODAY
|
||||
actionable_statuses = {"PENDING", "IN_PROGRESS", "SCHEDULED"}
|
||||
filtered_batches = []
|
||||
|
||||
for batch in result["batches"]:
|
||||
# Check if batch is actionable
|
||||
if batch.get("status") not in actionable_statuses:
|
||||
continue
|
||||
|
||||
# Check if batch is scheduled for today
|
||||
# Include batches that START today OR END today (for overnight batches)
|
||||
planned_start = batch.get("planned_start_time")
|
||||
planned_end = batch.get("planned_end_time")
|
||||
|
||||
include_batch = False
|
||||
|
||||
if planned_start:
|
||||
# Parse the start date string
|
||||
if isinstance(planned_start, str):
|
||||
planned_start = datetime.fromisoformat(planned_start.replace('Z', '+00:00'))
|
||||
|
||||
# Include if batch starts today
|
||||
if today_start <= planned_start < today_end:
|
||||
include_batch = True
|
||||
|
||||
# Also check if batch ends today (for overnight batches)
|
||||
if not include_batch and planned_end:
|
||||
if isinstance(planned_end, str):
|
||||
planned_end = datetime.fromisoformat(planned_end.replace('Z', '+00:00'))
|
||||
|
||||
# Include if batch ends today (even if it started yesterday)
|
||||
if today_start <= planned_end < today_end:
|
||||
include_batch = True
|
||||
|
||||
if include_batch:
|
||||
filtered_batches.append(batch)
|
||||
|
||||
# Return filtered result
|
||||
return {
|
||||
**result,
|
||||
"batches": filtered_batches,
|
||||
"total_count": len(filtered_batches)
|
||||
}
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error fetching today's batches", error=str(e), tenant_id=tenant_id)
|
||||
return None
|
||||
|
||||
@@ -125,33 +125,118 @@ class ProductionBatchReasoningData(BaseModel):
|
||||
|
||||
def create_po_reasoning_low_stock(
|
||||
supplier_name: str,
|
||||
product_names: list,
|
||||
current_stock: float,
|
||||
required_stock: float,
|
||||
days_until_stockout: int,
|
||||
product_names: list, # Kept for backward compatibility
|
||||
current_stock: float = None, # Kept for backward compatibility
|
||||
required_stock: float = None, # Kept for backward compatibility
|
||||
days_until_stockout: int = None, # Kept for backward compatibility
|
||||
threshold_percentage: int = 20,
|
||||
affected_products: Optional[list] = None,
|
||||
estimated_lost_orders: Optional[int] = None
|
||||
estimated_lost_orders: Optional[int] = None,
|
||||
# New enhanced parameters
|
||||
product_details: Optional[list] = None,
|
||||
supplier_lead_time_days: Optional[int] = None,
|
||||
order_urgency: Optional[str] = None,
|
||||
affected_production_batches: Optional[list] = None,
|
||||
estimated_production_loss_eur: Optional[float] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create reasoning data for low stock detection
|
||||
Create reasoning data for low stock detection with supply chain intelligence
|
||||
|
||||
Supports two modes:
|
||||
1. Legacy mode: Uses product_names, current_stock, days_until_stockout (simple)
|
||||
2. Enhanced mode: Uses product_details with per-product depletion analysis
|
||||
|
||||
Args:
|
||||
supplier_name: Name of the supplier
|
||||
product_names: List of product names in the order
|
||||
current_stock: Current stock level
|
||||
required_stock: Required stock level
|
||||
days_until_stockout: Days until stock runs out
|
||||
product_names: List of product names (legacy, for backward compatibility)
|
||||
current_stock: Current stock level (legacy)
|
||||
required_stock: Required stock level (legacy)
|
||||
days_until_stockout: Days until stock runs out (legacy)
|
||||
threshold_percentage: Stock threshold percentage
|
||||
affected_products: Products that will be affected
|
||||
estimated_lost_orders: Estimated number of lost orders
|
||||
affected_products: Products that will be affected (legacy)
|
||||
estimated_lost_orders: Estimated number of lost orders (legacy)
|
||||
product_details: List of dicts with per-product analysis (NEW):
|
||||
[
|
||||
{
|
||||
"product_name": str,
|
||||
"current_stock_kg": float,
|
||||
"daily_consumption_kg": float,
|
||||
"days_until_depletion": float,
|
||||
"reorder_point_kg": float,
|
||||
"safety_stock_days": int,
|
||||
"criticality": str # "critical", "urgent", "important", "normal"
|
||||
},
|
||||
...
|
||||
]
|
||||
supplier_lead_time_days: Supplier delivery lead time in days (NEW)
|
||||
order_urgency: Overall order urgency: "critical", "urgent", "important", "normal" (NEW)
|
||||
affected_production_batches: List of batch numbers that will be impacted (NEW)
|
||||
estimated_production_loss_eur: Estimated financial loss if not ordered (NEW)
|
||||
|
||||
Returns:
|
||||
Reasoning data dictionary
|
||||
Reasoning data dictionary with enhanced supply chain intelligence
|
||||
"""
|
||||
return {
|
||||
"type": PurchaseOrderReasoningType.LOW_STOCK_DETECTION.value,
|
||||
"parameters": {
|
||||
# Determine mode based on parameters
|
||||
enhanced_mode = product_details is not None
|
||||
|
||||
if enhanced_mode:
|
||||
# Enhanced mode: Use detailed per-product analysis
|
||||
# Extract critical products for summary
|
||||
critical_products = [
|
||||
p for p in product_details
|
||||
if p.get("criticality") in ["critical", "urgent"]
|
||||
]
|
||||
|
||||
# Find most critical depletion time
|
||||
min_depletion_days = min(
|
||||
[p.get("days_until_depletion", 999) for p in product_details],
|
||||
default=7
|
||||
)
|
||||
min_depletion_hours = round(min_depletion_days * 24, 1)
|
||||
|
||||
# Calculate safety margin with supplier lead time
|
||||
safety_margin_days = None
|
||||
if supplier_lead_time_days is not None:
|
||||
safety_margin_days = min_depletion_days - supplier_lead_time_days
|
||||
|
||||
# All product names for backward compatibility
|
||||
all_product_names = [p.get("product_name", "Product") for p in product_details]
|
||||
|
||||
parameters = {
|
||||
"supplier_name": supplier_name,
|
||||
"supplier_lead_time_days": supplier_lead_time_days or 2,
|
||||
"product_details": product_details,
|
||||
"product_names": all_product_names, # For i18n join operations
|
||||
"product_count": len(product_details),
|
||||
"critical_products": [p.get("product_name") for p in critical_products],
|
||||
"critical_product_count": len(critical_products),
|
||||
"min_depletion_days": round(min_depletion_days, 2),
|
||||
"min_depletion_hours": min_depletion_hours,
|
||||
"safety_margin_days": round(safety_margin_days, 2) if safety_margin_days is not None else None,
|
||||
"order_urgency": order_urgency or "normal",
|
||||
"affected_batches": affected_production_batches or [],
|
||||
"affected_batches_count": len(affected_production_batches) if affected_production_batches else 0,
|
||||
"potential_loss_eur": round(estimated_production_loss_eur, 2) if estimated_production_loss_eur else 0,
|
||||
"threshold_percentage": threshold_percentage
|
||||
}
|
||||
|
||||
# Enhanced consequence calculation
|
||||
severity = ConsequenceSeverity.CRITICAL.value if order_urgency == "critical" else \
|
||||
ConsequenceSeverity.HIGH.value if order_urgency in ["urgent", "important"] else \
|
||||
ConsequenceSeverity.MEDIUM.value
|
||||
|
||||
consequence = {
|
||||
"type": "stockout_risk_detailed",
|
||||
"severity": severity,
|
||||
"impact_days": round(min_depletion_days, 2),
|
||||
"affected_production_batches": affected_production_batches or [],
|
||||
"estimated_production_loss_eur": round(estimated_production_loss_eur, 2) if estimated_production_loss_eur else 0,
|
||||
"critical_items": [p.get("product_name") for p in critical_products]
|
||||
}
|
||||
|
||||
else:
|
||||
# Legacy mode: Use simple parameters (backward compatibility)
|
||||
parameters = {
|
||||
"supplier_name": supplier_name,
|
||||
"product_names": product_names,
|
||||
"product_count": len(product_names),
|
||||
@@ -159,18 +244,25 @@ def create_po_reasoning_low_stock(
|
||||
"required_stock": required_stock,
|
||||
"days_until_stockout": days_until_stockout,
|
||||
"threshold_percentage": threshold_percentage,
|
||||
"stock_percentage": round((current_stock / required_stock * 100), 1) if required_stock > 0 else 0
|
||||
},
|
||||
"consequence": {
|
||||
"stock_percentage": round((current_stock / required_stock * 100), 1) if (required_stock and required_stock > 0) else 0
|
||||
}
|
||||
|
||||
consequence = {
|
||||
"type": "stockout_risk",
|
||||
"severity": ConsequenceSeverity.HIGH.value if days_until_stockout <= 2 else ConsequenceSeverity.MEDIUM.value,
|
||||
"impact_days": days_until_stockout,
|
||||
"severity": ConsequenceSeverity.HIGH.value if days_until_stockout and days_until_stockout <= 2 else ConsequenceSeverity.MEDIUM.value,
|
||||
"impact_days": days_until_stockout or 7,
|
||||
"affected_products": affected_products or [],
|
||||
"estimated_lost_orders": estimated_lost_orders or 0
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
"type": PurchaseOrderReasoningType.LOW_STOCK_DETECTION.value,
|
||||
"parameters": parameters,
|
||||
"consequence": consequence,
|
||||
"metadata": {
|
||||
"trigger_source": "orchestrator_auto",
|
||||
"ai_assisted": True
|
||||
"ai_assisted": True,
|
||||
"enhanced_mode": enhanced_mode
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user