New alert system and panel de control page
This commit is contained in:
948
shared/alerts/context_templates.py
Normal file
948
shared/alerts/context_templates.py
Normal file
@@ -0,0 +1,948 @@
|
||||
"""
|
||||
Context-Aware Alert Message Templates with i18n Support
|
||||
|
||||
This module generates parametrized alert messages that can be translated by the frontend.
|
||||
Instead of hardcoded Spanish messages, we generate structured message keys and parameters.
|
||||
|
||||
Messages are generated AFTER enrichment, leveraging:
|
||||
- Orchestrator context (AI actions already taken)
|
||||
- Business impact (financial, customers affected)
|
||||
- Urgency context (hours until consequence, actual dates)
|
||||
- User agency (supplier contacts, external dependencies)
|
||||
|
||||
Frontend uses i18n to translate message keys with parameters.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, Optional, List
|
||||
from shared.schemas.alert_types import (
|
||||
EnrichedAlert, OrchestratorContext, BusinessImpact,
|
||||
UrgencyContext, UserAgency, TrendContext, SmartAction
|
||||
)
|
||||
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
def format_date_spanish(dt: datetime) -> str:
|
||||
"""Format datetime in Spanish (for backwards compatibility)"""
|
||||
days = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"]
|
||||
months = ["enero", "febrero", "marzo", "abril", "mayo", "junio",
|
||||
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]
|
||||
|
||||
day_name = days[dt.weekday()]
|
||||
month_name = months[dt.month - 1]
|
||||
|
||||
return f"{day_name} {dt.day} de {month_name}"
|
||||
|
||||
|
||||
def format_iso_date(dt: datetime) -> str:
|
||||
"""Format datetime as ISO date for frontend i18n"""
|
||||
return dt.strftime('%Y-%m-%d')
|
||||
|
||||
|
||||
def get_production_date(metadata: Dict[str, Any], default_days: int = 1) -> datetime:
|
||||
"""Get actual production date from metadata or estimate"""
|
||||
if metadata.get('production_date'):
|
||||
if isinstance(metadata['production_date'], str):
|
||||
return datetime.fromisoformat(metadata['production_date'])
|
||||
return metadata['production_date']
|
||||
else:
|
||||
return datetime.now() + timedelta(days=default_days)
|
||||
|
||||
|
||||
class ContextualMessageGenerator:
|
||||
"""Generates context-aware parametrized messages for i18n"""
|
||||
|
||||
@staticmethod
|
||||
def generate_message_data(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate contextual message data with i18n support
|
||||
|
||||
Returns dict with:
|
||||
- title_key: i18n key for title
|
||||
- title_params: parameters for title translation
|
||||
- message_key: i18n key for message
|
||||
- message_params: parameters for message translation
|
||||
- fallback_title: fallback if i18n not available
|
||||
- fallback_message: fallback if i18n not available
|
||||
"""
|
||||
alert_type = enriched.alert_type
|
||||
|
||||
# Dispatch to specific generator based on alert type
|
||||
generators = {
|
||||
# Inventory alerts
|
||||
'critical_stock_shortage': ContextualMessageGenerator._stock_shortage,
|
||||
'low_stock_warning': ContextualMessageGenerator._low_stock,
|
||||
'stock_depleted_by_order': ContextualMessageGenerator._stock_depleted,
|
||||
'production_ingredient_shortage': ContextualMessageGenerator._ingredient_shortage,
|
||||
'expired_products': ContextualMessageGenerator._expired_products,
|
||||
|
||||
# Production alerts
|
||||
'production_delay': ContextualMessageGenerator._production_delay,
|
||||
'equipment_failure': ContextualMessageGenerator._equipment_failure,
|
||||
'maintenance_required': ContextualMessageGenerator._maintenance_required,
|
||||
'low_equipment_efficiency': ContextualMessageGenerator._low_efficiency,
|
||||
'order_overload': ContextualMessageGenerator._order_overload,
|
||||
|
||||
# Supplier alerts
|
||||
'supplier_delay': ContextualMessageGenerator._supplier_delay,
|
||||
|
||||
# Procurement alerts
|
||||
'po_approval_needed': ContextualMessageGenerator._po_approval_needed,
|
||||
'production_batch_start': ContextualMessageGenerator._production_batch_start,
|
||||
|
||||
# Environmental alerts
|
||||
'temperature_breach': ContextualMessageGenerator._temperature_breach,
|
||||
|
||||
# Forecasting alerts
|
||||
'demand_surge_weekend': ContextualMessageGenerator._demand_surge,
|
||||
'weather_impact_alert': ContextualMessageGenerator._weather_impact,
|
||||
'holiday_preparation': ContextualMessageGenerator._holiday_prep,
|
||||
'severe_weather_impact': ContextualMessageGenerator._severe_weather,
|
||||
'unexpected_demand_spike': ContextualMessageGenerator._demand_spike,
|
||||
'demand_pattern_optimization': ContextualMessageGenerator._demand_pattern,
|
||||
|
||||
# Recommendations
|
||||
'inventory_optimization': ContextualMessageGenerator._inventory_optimization,
|
||||
'production_efficiency': ContextualMessageGenerator._production_efficiency,
|
||||
'sales_opportunity': ContextualMessageGenerator._sales_opportunity,
|
||||
'seasonal_adjustment': ContextualMessageGenerator._seasonal_adjustment,
|
||||
'cost_reduction': ContextualMessageGenerator._cost_reduction,
|
||||
'waste_reduction': ContextualMessageGenerator._waste_reduction,
|
||||
'quality_improvement': ContextualMessageGenerator._quality_improvement,
|
||||
'customer_satisfaction': ContextualMessageGenerator._customer_satisfaction,
|
||||
'energy_optimization': ContextualMessageGenerator._energy_optimization,
|
||||
'staff_optimization': ContextualMessageGenerator._staff_optimization,
|
||||
}
|
||||
|
||||
generator_func = generators.get(alert_type)
|
||||
if generator_func:
|
||||
return generator_func(enriched)
|
||||
else:
|
||||
# Fallback for unknown alert types
|
||||
return {
|
||||
'title_key': f'alerts.{alert_type}.title',
|
||||
'title_params': {},
|
||||
'message_key': f'alerts.{alert_type}.message',
|
||||
'message_params': {'alert_type': alert_type},
|
||||
'fallback_title': f"Alerta: {alert_type}",
|
||||
'fallback_message': f"Se detectó una situación que requiere atención: {alert_type}"
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# INVENTORY ALERTS
|
||||
# ===================================================================
|
||||
|
||||
@staticmethod
|
||||
def _stock_shortage(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Critical stock shortage with AI context"""
|
||||
metadata = enriched.alert_metadata
|
||||
orch = enriched.orchestrator_context
|
||||
urgency = enriched.urgency_context
|
||||
agency = enriched.user_agency
|
||||
|
||||
ingredient_name = metadata.get('ingredient_name', 'Ingrediente')
|
||||
current_stock = round(metadata.get('current_stock', 0), 1)
|
||||
required_stock = round(metadata.get('required_stock', metadata.get('tomorrow_needed', 0)), 1)
|
||||
|
||||
# Base parameters
|
||||
params = {
|
||||
'ingredient_name': ingredient_name,
|
||||
'current_stock': current_stock,
|
||||
'required_stock': required_stock
|
||||
}
|
||||
|
||||
# Determine message variant based on context
|
||||
if orch and orch.already_addressed and orch.action_type == "purchase_order":
|
||||
# AI already created PO
|
||||
params['po_id'] = orch.action_id
|
||||
params['po_amount'] = metadata.get('po_amount', 0)
|
||||
|
||||
if orch.delivery_date:
|
||||
params['delivery_date'] = format_iso_date(orch.delivery_date)
|
||||
params['delivery_day_name'] = format_date_spanish(orch.delivery_date)
|
||||
|
||||
if orch.action_status == "pending_approval":
|
||||
message_key = 'alerts.critical_stock_shortage.message_with_po_pending'
|
||||
else:
|
||||
message_key = 'alerts.critical_stock_shortage.message_with_po_created'
|
||||
|
||||
elif urgency and urgency.time_until_consequence_hours:
|
||||
# Time-specific message
|
||||
hours = urgency.time_until_consequence_hours
|
||||
params['hours_until'] = round(hours, 1)
|
||||
message_key = 'alerts.critical_stock_shortage.message_with_hours'
|
||||
|
||||
elif metadata.get('production_date'):
|
||||
# Date-specific message
|
||||
prod_date = get_production_date(metadata)
|
||||
params['production_date'] = format_iso_date(prod_date)
|
||||
params['production_day_name'] = format_date_spanish(prod_date)
|
||||
message_key = 'alerts.critical_stock_shortage.message_with_date'
|
||||
|
||||
else:
|
||||
# Generic message
|
||||
message_key = 'alerts.critical_stock_shortage.message_generic'
|
||||
|
||||
# Add supplier contact if available
|
||||
if agency and agency.requires_external_party and agency.external_party_contact:
|
||||
params['supplier_name'] = agency.external_party_name
|
||||
params['supplier_contact'] = agency.external_party_contact
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.critical_stock_shortage.title',
|
||||
'title_params': {'ingredient_name': ingredient_name},
|
||||
'message_key': message_key,
|
||||
'message_params': params,
|
||||
'fallback_title': f"🚨 Stock Crítico: {ingredient_name}",
|
||||
'fallback_message': f"Solo {current_stock}kg de {ingredient_name} disponibles (necesitas {required_stock}kg)."
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _low_stock(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Low stock warning"""
|
||||
metadata = enriched.alert_metadata
|
||||
orch = enriched.orchestrator_context
|
||||
|
||||
ingredient_name = metadata.get('ingredient_name', 'Ingrediente')
|
||||
current_stock = round(metadata.get('current_stock', 0), 1)
|
||||
minimum_stock = round(metadata.get('minimum_stock', 0), 1)
|
||||
|
||||
params = {
|
||||
'ingredient_name': ingredient_name,
|
||||
'current_stock': current_stock,
|
||||
'minimum_stock': minimum_stock
|
||||
}
|
||||
|
||||
if orch and orch.already_addressed and orch.action_type == "purchase_order":
|
||||
params['po_id'] = orch.action_id
|
||||
message_key = 'alerts.low_stock.message_with_po'
|
||||
else:
|
||||
message_key = 'alerts.low_stock.message_generic'
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.low_stock.title',
|
||||
'title_params': {'ingredient_name': ingredient_name},
|
||||
'message_key': message_key,
|
||||
'message_params': params,
|
||||
'fallback_title': f"⚠️ Stock Bajo: {ingredient_name}",
|
||||
'fallback_message': f"Stock de {ingredient_name}: {current_stock}kg (mínimo: {minimum_stock}kg)."
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _stock_depleted(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Stock depleted by order"""
|
||||
metadata = enriched.alert_metadata
|
||||
agency = enriched.user_agency
|
||||
|
||||
ingredient_name = metadata.get('ingredient_name', 'Ingrediente')
|
||||
order_id = metadata.get('order_id', '???')
|
||||
current_stock = round(metadata.get('current_stock', 0), 1)
|
||||
minimum_stock = round(metadata.get('minimum_stock', 0), 1)
|
||||
|
||||
params = {
|
||||
'ingredient_name': ingredient_name,
|
||||
'order_id': order_id,
|
||||
'current_stock': current_stock,
|
||||
'minimum_stock': minimum_stock
|
||||
}
|
||||
|
||||
if agency and agency.requires_external_party and agency.external_party_contact:
|
||||
params['supplier_name'] = agency.external_party_name
|
||||
params['supplier_contact'] = agency.external_party_contact
|
||||
message_key = 'alerts.stock_depleted.message_with_supplier'
|
||||
else:
|
||||
message_key = 'alerts.stock_depleted.message_generic'
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.stock_depleted.title',
|
||||
'title_params': {'ingredient_name': ingredient_name},
|
||||
'message_key': message_key,
|
||||
'message_params': params,
|
||||
'fallback_title': f"⚠️ Stock Agotado por Pedido: {ingredient_name}",
|
||||
'fallback_message': f"El pedido #{order_id} agotaría el stock de {ingredient_name}."
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _ingredient_shortage(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Production ingredient shortage"""
|
||||
metadata = enriched.alert_metadata
|
||||
impact = enriched.business_impact
|
||||
|
||||
ingredient_name = metadata.get('ingredient_name', 'Ingrediente')
|
||||
shortage_amount = round(metadata.get('shortage_amount', 0), 1)
|
||||
affected_batches = metadata.get('affected_batches_count', 0)
|
||||
|
||||
params = {
|
||||
'ingredient_name': ingredient_name,
|
||||
'shortage_amount': shortage_amount,
|
||||
'affected_batches': affected_batches
|
||||
}
|
||||
|
||||
if impact and impact.affected_customers:
|
||||
params['customer_count'] = len(impact.affected_customers)
|
||||
params['customer_names'] = ', '.join(impact.affected_customers[:2])
|
||||
if len(impact.affected_customers) > 2:
|
||||
params['additional_customers'] = len(impact.affected_customers) - 2
|
||||
message_key = 'alerts.ingredient_shortage.message_with_customers'
|
||||
else:
|
||||
message_key = 'alerts.ingredient_shortage.message_generic'
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.ingredient_shortage.title',
|
||||
'title_params': {'ingredient_name': ingredient_name},
|
||||
'message_key': message_key,
|
||||
'message_params': params,
|
||||
'fallback_title': f"🚨 Escasez en Producción: {ingredient_name}",
|
||||
'fallback_message': f"Faltan {shortage_amount}kg de {ingredient_name}."
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _expired_products(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Expired products alert"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
product_count = metadata.get('product_count', 0)
|
||||
expired_items = metadata.get('expired_items', [])
|
||||
|
||||
params = {
|
||||
'product_count': product_count
|
||||
}
|
||||
|
||||
if len(expired_items) > 0:
|
||||
params['product_names'] = ', '.join([item.get('name', 'Producto') for item in expired_items[:2]])
|
||||
if len(expired_items) > 2:
|
||||
params['additional_count'] = len(expired_items) - 2
|
||||
message_key = 'alerts.expired_products.message_with_names'
|
||||
else:
|
||||
message_key = 'alerts.expired_products.message_generic'
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.expired_products.title',
|
||||
'title_params': {},
|
||||
'message_key': message_key,
|
||||
'message_params': params,
|
||||
'fallback_title': "📅 Productos Caducados",
|
||||
'fallback_message': f"{product_count} producto(s) caducado(s). Retirar inmediatamente."
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# PRODUCTION ALERTS
|
||||
# ===================================================================
|
||||
|
||||
@staticmethod
|
||||
def _production_delay(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Production delay with affected orders"""
|
||||
metadata = enriched.alert_metadata
|
||||
impact = enriched.business_impact
|
||||
|
||||
batch_name = metadata.get('batch_name', 'Lote')
|
||||
delay_minutes = metadata.get('delay_minutes', 0)
|
||||
|
||||
params = {
|
||||
'batch_name': batch_name,
|
||||
'delay_minutes': delay_minutes
|
||||
}
|
||||
|
||||
if impact and impact.affected_customers:
|
||||
params['customer_names'] = ', '.join(impact.affected_customers[:2])
|
||||
if len(impact.affected_customers) > 2:
|
||||
params['additional_count'] = len(impact.affected_customers) - 2
|
||||
message_key = 'alerts.production_delay.message_with_customers'
|
||||
elif impact and impact.affected_orders:
|
||||
params['affected_orders'] = impact.affected_orders
|
||||
message_key = 'alerts.production_delay.message_with_orders'
|
||||
else:
|
||||
message_key = 'alerts.production_delay.message_generic'
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.production_delay.title',
|
||||
'title_params': {},
|
||||
'message_key': message_key,
|
||||
'message_params': params,
|
||||
'fallback_title': "⏰ Retraso en Producción",
|
||||
'fallback_message': f"Lote {batch_name} con {delay_minutes} minutos de retraso."
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _equipment_failure(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Equipment failure"""
|
||||
metadata = enriched.alert_metadata
|
||||
impact = enriched.business_impact
|
||||
|
||||
equipment_name = metadata.get('equipment_name', 'Equipo')
|
||||
|
||||
params = {
|
||||
'equipment_name': equipment_name
|
||||
}
|
||||
|
||||
if impact and impact.production_batches_at_risk:
|
||||
params['batch_count'] = len(impact.production_batches_at_risk)
|
||||
message_key = 'alerts.equipment_failure.message_with_batches'
|
||||
else:
|
||||
message_key = 'alerts.equipment_failure.message_generic'
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.equipment_failure.title',
|
||||
'title_params': {'equipment_name': equipment_name},
|
||||
'message_key': message_key,
|
||||
'message_params': params,
|
||||
'fallback_title': f"⚙️ Fallo de Equipo: {equipment_name}",
|
||||
'fallback_message': f"{equipment_name} no está funcionando correctamente."
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _maintenance_required(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Maintenance required"""
|
||||
metadata = enriched.alert_metadata
|
||||
urgency = enriched.urgency_context
|
||||
|
||||
equipment_name = metadata.get('equipment_name', 'Equipo')
|
||||
|
||||
params = {
|
||||
'equipment_name': equipment_name
|
||||
}
|
||||
|
||||
if urgency and urgency.time_until_consequence_hours:
|
||||
params['hours_until'] = round(urgency.time_until_consequence_hours, 1)
|
||||
message_key = 'alerts.maintenance_required.message_with_hours'
|
||||
else:
|
||||
params['days_until'] = metadata.get('days_until_maintenance', 0)
|
||||
message_key = 'alerts.maintenance_required.message_with_days'
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.maintenance_required.title',
|
||||
'title_params': {'equipment_name': equipment_name},
|
||||
'message_key': message_key,
|
||||
'message_params': params,
|
||||
'fallback_title': f"🔧 Mantenimiento Requerido: {equipment_name}",
|
||||
'fallback_message': f"Equipo {equipment_name} requiere mantenimiento."
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _low_efficiency(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Low equipment efficiency"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
equipment_name = metadata.get('equipment_name', 'Equipo')
|
||||
efficiency = round(metadata.get('efficiency_percent', 0), 1)
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.low_efficiency.title',
|
||||
'title_params': {'equipment_name': equipment_name},
|
||||
'message_key': 'alerts.low_efficiency.message',
|
||||
'message_params': {
|
||||
'equipment_name': equipment_name,
|
||||
'efficiency_percent': efficiency
|
||||
},
|
||||
'fallback_title': f"📉 Baja Eficiencia: {equipment_name}",
|
||||
'fallback_message': f"Eficiencia del {equipment_name} bajó a {efficiency}%."
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _order_overload(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Order capacity overload"""
|
||||
metadata = enriched.alert_metadata
|
||||
impact = enriched.business_impact
|
||||
|
||||
percentage = round(metadata.get('percentage', 0), 1)
|
||||
|
||||
params = {
|
||||
'percentage': percentage
|
||||
}
|
||||
|
||||
if impact and impact.affected_orders:
|
||||
params['affected_orders'] = impact.affected_orders
|
||||
message_key = 'alerts.order_overload.message_with_orders'
|
||||
else:
|
||||
message_key = 'alerts.order_overload.message_generic'
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.order_overload.title',
|
||||
'title_params': {},
|
||||
'message_key': message_key,
|
||||
'message_params': params,
|
||||
'fallback_title': "📋 Sobrecarga de Pedidos",
|
||||
'fallback_message': f"Capacidad excedida en {percentage}%."
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# SUPPLIER ALERTS
|
||||
# ===================================================================
|
||||
|
||||
@staticmethod
|
||||
def _supplier_delay(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Supplier delivery delay"""
|
||||
metadata = enriched.alert_metadata
|
||||
impact = enriched.business_impact
|
||||
agency = enriched.user_agency
|
||||
|
||||
supplier_name = metadata.get('supplier_name', 'Proveedor')
|
||||
hours = round(metadata.get('hours', metadata.get('delay_hours', 0)), 0)
|
||||
products = metadata.get('products', metadata.get('affected_products', ''))
|
||||
|
||||
params = {
|
||||
'supplier_name': supplier_name,
|
||||
'hours': hours,
|
||||
'products': products
|
||||
}
|
||||
|
||||
if impact and impact.production_batches_at_risk:
|
||||
params['batch_count'] = len(impact.production_batches_at_risk)
|
||||
|
||||
if agency and agency.external_party_contact:
|
||||
params['supplier_contact'] = agency.external_party_contact
|
||||
|
||||
message_key = 'alerts.supplier_delay.message'
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.supplier_delay.title',
|
||||
'title_params': {'supplier_name': supplier_name},
|
||||
'message_key': message_key,
|
||||
'message_params': params,
|
||||
'fallback_title': f"🚚 Retraso de Proveedor: {supplier_name}",
|
||||
'fallback_message': f"Entrega de {supplier_name} retrasada {hours} hora(s)."
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# PROCUREMENT ALERTS
|
||||
# ===================================================================
|
||||
|
||||
@staticmethod
|
||||
def _po_approval_needed(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Purchase order approval needed"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
po_number = metadata.get('po_number', 'PO-XXXX')
|
||||
supplier_name = metadata.get('supplier_name', 'Proveedor')
|
||||
total_amount = metadata.get('total_amount', 0)
|
||||
currency = metadata.get('currency', '€')
|
||||
required_delivery_date = metadata.get('required_delivery_date')
|
||||
|
||||
# Format required delivery date for i18n
|
||||
required_delivery_date_iso = None
|
||||
if required_delivery_date:
|
||||
if isinstance(required_delivery_date, str):
|
||||
try:
|
||||
dt = datetime.fromisoformat(required_delivery_date.replace('Z', '+00:00'))
|
||||
required_delivery_date_iso = format_iso_date(dt)
|
||||
except:
|
||||
required_delivery_date_iso = required_delivery_date
|
||||
elif isinstance(required_delivery_date, datetime):
|
||||
required_delivery_date_iso = format_iso_date(required_delivery_date)
|
||||
|
||||
params = {
|
||||
'po_number': po_number,
|
||||
'supplier_name': supplier_name,
|
||||
'total_amount': round(total_amount, 2),
|
||||
'currency': currency,
|
||||
'required_delivery_date': required_delivery_date_iso or 'fecha no especificada'
|
||||
}
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.po_approval_needed.title',
|
||||
'title_params': {'po_number': po_number},
|
||||
'message_key': 'alerts.po_approval_needed.message',
|
||||
'message_params': params
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _production_batch_start(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Production batch ready to start"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
batch_number = metadata.get('batch_number', 'BATCH-XXXX')
|
||||
product_name = metadata.get('product_name', 'Producto')
|
||||
quantity_planned = metadata.get('quantity_planned', 0)
|
||||
unit = metadata.get('unit', 'kg')
|
||||
priority = metadata.get('priority', 'normal')
|
||||
|
||||
params = {
|
||||
'batch_number': batch_number,
|
||||
'product_name': product_name,
|
||||
'quantity_planned': round(quantity_planned, 1),
|
||||
'unit': unit,
|
||||
'priority': priority
|
||||
}
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.production_batch_start.title',
|
||||
'title_params': {'product_name': product_name},
|
||||
'message_key': 'alerts.production_batch_start.message',
|
||||
'message_params': params
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# ENVIRONMENTAL ALERTS
|
||||
# ===================================================================
|
||||
|
||||
@staticmethod
|
||||
def _temperature_breach(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Temperature breach alert"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
location = metadata.get('location', 'Ubicación')
|
||||
temperature = round(metadata.get('temperature', 0), 1)
|
||||
duration = metadata.get('duration', 0)
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.temperature_breach.title',
|
||||
'title_params': {'location': location},
|
||||
'message_key': 'alerts.temperature_breach.message',
|
||||
'message_params': {
|
||||
'location': location,
|
||||
'temperature': temperature,
|
||||
'duration': duration
|
||||
}
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# FORECASTING ALERTS
|
||||
# ===================================================================
|
||||
|
||||
@staticmethod
|
||||
def _demand_surge(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Weekend demand surge"""
|
||||
metadata = enriched.alert_metadata
|
||||
urgency = enriched.urgency_context
|
||||
|
||||
product_name = metadata.get('product_name', 'Producto')
|
||||
percentage = round(metadata.get('percentage', metadata.get('growth_percentage', 0)), 0)
|
||||
predicted_demand = metadata.get('predicted_demand', 0)
|
||||
current_stock = metadata.get('current_stock', 0)
|
||||
|
||||
params = {
|
||||
'product_name': product_name,
|
||||
'percentage': percentage
|
||||
}
|
||||
|
||||
if predicted_demand and current_stock:
|
||||
params['predicted_demand'] = round(predicted_demand, 0)
|
||||
params['current_stock'] = round(current_stock, 0)
|
||||
|
||||
if urgency and urgency.time_until_consequence_hours:
|
||||
params['hours_until'] = round(urgency.time_until_consequence_hours, 1)
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.demand_surge.title',
|
||||
'title_params': {'product_name': product_name},
|
||||
'message_key': 'alerts.demand_surge.message',
|
||||
'message_params': params
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _weather_impact(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Weather impact on demand"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
weather_type = metadata.get('weather_type', 'Lluvia')
|
||||
impact_percentage = round(metadata.get('impact_percentage', -20), 0)
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.weather_impact.title',
|
||||
'title_params': {},
|
||||
'message_key': 'alerts.weather_impact.message',
|
||||
'message_params': {
|
||||
'weather_type': weather_type,
|
||||
'impact_percentage': abs(impact_percentage),
|
||||
'is_negative': impact_percentage < 0
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _holiday_prep(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Holiday preparation"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
holiday_name = metadata.get('holiday_name', 'Festividad')
|
||||
days = metadata.get('days', 0)
|
||||
percentage = round(metadata.get('percentage', metadata.get('increase_percentage', 0)), 0)
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.holiday_prep.title',
|
||||
'title_params': {'holiday_name': holiday_name},
|
||||
'message_key': 'alerts.holiday_prep.message',
|
||||
'message_params': {
|
||||
'holiday_name': holiday_name,
|
||||
'days': days,
|
||||
'percentage': percentage
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _severe_weather(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Severe weather impact"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
weather_type = metadata.get('weather_type', 'Tormenta')
|
||||
duration_hours = metadata.get('duration_hours', 0)
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.severe_weather.title',
|
||||
'title_params': {'weather_type': weather_type},
|
||||
'message_key': 'alerts.severe_weather.message',
|
||||
'message_params': {
|
||||
'weather_type': weather_type,
|
||||
'duration_hours': duration_hours
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _demand_spike(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Unexpected demand spike"""
|
||||
metadata = enriched.alert_metadata
|
||||
trend = enriched.trend_context
|
||||
|
||||
product_name = metadata.get('product_name', 'Producto')
|
||||
spike_percentage = round(metadata.get('spike_percentage', metadata.get('growth_percentage', 0)), 0)
|
||||
|
||||
params = {
|
||||
'product_name': product_name,
|
||||
'spike_percentage': spike_percentage
|
||||
}
|
||||
|
||||
if trend:
|
||||
params['current_value'] = round(trend.current_value, 0)
|
||||
params['baseline_value'] = round(trend.baseline_value, 0)
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.demand_spike.title',
|
||||
'title_params': {'product_name': product_name},
|
||||
'message_key': 'alerts.demand_spike.message',
|
||||
'message_params': params
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _demand_pattern(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Demand pattern optimization"""
|
||||
metadata = enriched.alert_metadata
|
||||
trend = enriched.trend_context
|
||||
|
||||
product_name = metadata.get('product_name', 'Producto')
|
||||
variation = round(metadata.get('variation_percent', 0), 0)
|
||||
|
||||
params = {
|
||||
'product_name': product_name,
|
||||
'variation_percent': variation
|
||||
}
|
||||
|
||||
if trend and trend.possible_causes:
|
||||
params['possible_causes'] = ', '.join(trend.possible_causes[:2])
|
||||
|
||||
return {
|
||||
'title_key': 'alerts.demand_pattern.title',
|
||||
'title_params': {'product_name': product_name},
|
||||
'message_key': 'alerts.demand_pattern.message',
|
||||
'message_params': params
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# RECOMMENDATIONS
|
||||
# ===================================================================
|
||||
|
||||
@staticmethod
|
||||
def _inventory_optimization(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Inventory optimization recommendation"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
ingredient_name = metadata.get('ingredient_name', 'Ingrediente')
|
||||
period = metadata.get('period', 7)
|
||||
suggested_increase = round(metadata.get('suggested_increase', 0), 1)
|
||||
|
||||
return {
|
||||
'title_key': 'recommendations.inventory_optimization.title',
|
||||
'title_params': {'ingredient_name': ingredient_name},
|
||||
'message_key': 'recommendations.inventory_optimization.message',
|
||||
'message_params': {
|
||||
'ingredient_name': ingredient_name,
|
||||
'period': period,
|
||||
'suggested_increase': suggested_increase
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _production_efficiency(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Production efficiency recommendation"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
suggested_time = metadata.get('suggested_time', '')
|
||||
savings_percent = round(metadata.get('savings_percent', 0), 1)
|
||||
|
||||
return {
|
||||
'title_key': 'recommendations.production_efficiency.title',
|
||||
'title_params': {},
|
||||
'message_key': 'recommendations.production_efficiency.message',
|
||||
'message_params': {
|
||||
'suggested_time': suggested_time,
|
||||
'savings_percent': savings_percent
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _sales_opportunity(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Sales opportunity recommendation"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
product_name = metadata.get('product_name', 'Producto')
|
||||
days = metadata.get('days', '')
|
||||
increase_percent = round(metadata.get('increase_percent', 0), 0)
|
||||
|
||||
return {
|
||||
'title_key': 'recommendations.sales_opportunity.title',
|
||||
'title_params': {'product_name': product_name},
|
||||
'message_key': 'recommendations.sales_opportunity.message',
|
||||
'message_params': {
|
||||
'product_name': product_name,
|
||||
'days': days,
|
||||
'increase_percent': increase_percent
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _seasonal_adjustment(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Seasonal adjustment recommendation"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
season = metadata.get('season', 'temporada')
|
||||
products = metadata.get('products', 'productos estacionales')
|
||||
|
||||
return {
|
||||
'title_key': 'recommendations.seasonal_adjustment.title',
|
||||
'title_params': {},
|
||||
'message_key': 'recommendations.seasonal_adjustment.message',
|
||||
'message_params': {
|
||||
'season': season,
|
||||
'products': products
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _cost_reduction(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Cost reduction recommendation"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
supplier_name = metadata.get('supplier_name', 'Proveedor')
|
||||
ingredient = metadata.get('ingredient', 'ingrediente')
|
||||
savings_euros = round(metadata.get('savings_euros', 0), 0)
|
||||
|
||||
return {
|
||||
'title_key': 'recommendations.cost_reduction.title',
|
||||
'title_params': {},
|
||||
'message_key': 'recommendations.cost_reduction.message',
|
||||
'message_params': {
|
||||
'supplier_name': supplier_name,
|
||||
'ingredient': ingredient,
|
||||
'savings_euros': savings_euros
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _waste_reduction(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Waste reduction recommendation"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
product = metadata.get('product', 'producto')
|
||||
waste_reduction_percent = round(metadata.get('waste_reduction_percent', 0), 0)
|
||||
|
||||
return {
|
||||
'title_key': 'recommendations.waste_reduction.title',
|
||||
'title_params': {},
|
||||
'message_key': 'recommendations.waste_reduction.message',
|
||||
'message_params': {
|
||||
'product': product,
|
||||
'waste_reduction_percent': waste_reduction_percent
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _quality_improvement(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Quality improvement recommendation"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
product = metadata.get('product', 'producto')
|
||||
|
||||
return {
|
||||
'title_key': 'recommendations.quality_improvement.title',
|
||||
'title_params': {},
|
||||
'message_key': 'recommendations.quality_improvement.message',
|
||||
'message_params': {
|
||||
'product': product
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _customer_satisfaction(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Customer satisfaction recommendation"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
product = metadata.get('product', 'producto')
|
||||
days = metadata.get('days', '')
|
||||
|
||||
return {
|
||||
'title_key': 'recommendations.customer_satisfaction.title',
|
||||
'title_params': {},
|
||||
'message_key': 'recommendations.customer_satisfaction.message',
|
||||
'message_params': {
|
||||
'product': product,
|
||||
'days': days
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _energy_optimization(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Energy optimization recommendation"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
start_time = metadata.get('start_time', '')
|
||||
end_time = metadata.get('end_time', '')
|
||||
savings_euros = round(metadata.get('savings_euros', 0), 0)
|
||||
|
||||
return {
|
||||
'title_key': 'recommendations.energy_optimization.title',
|
||||
'title_params': {},
|
||||
'message_key': 'recommendations.energy_optimization.message',
|
||||
'message_params': {
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
'savings_euros': savings_euros
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _staff_optimization(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""Staff optimization recommendation"""
|
||||
metadata = enriched.alert_metadata
|
||||
|
||||
days = metadata.get('days', '')
|
||||
hours = metadata.get('hours', '')
|
||||
|
||||
return {
|
||||
'title_key': 'recommendations.staff_optimization.title',
|
||||
'title_params': {},
|
||||
'message_key': 'recommendations.staff_optimization.message',
|
||||
'message_params': {
|
||||
'days': days,
|
||||
'hours': hours
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def generate_contextual_message(enriched: EnrichedAlert) -> Dict[str, Any]:
|
||||
"""
|
||||
Main entry point for contextual message generation with i18n support
|
||||
|
||||
Args:
|
||||
enriched: Fully enriched alert with all context
|
||||
|
||||
Returns:
|
||||
Dict with:
|
||||
- title_key: i18n translation key for title
|
||||
- title_params: parameters for title translation
|
||||
- message_key: i18n translation key for message
|
||||
- message_params: parameters for message translation
|
||||
- fallback_title: fallback if i18n not available
|
||||
- fallback_message: fallback if i18n not available
|
||||
"""
|
||||
return ContextualMessageGenerator.generate_message_data(enriched)
|
||||
Reference in New Issue
Block a user