Improve the frontend

This commit is contained in:
Urtzi Alfaro
2025-10-21 19:50:07 +02:00
parent 05da20357d
commit 8d30172483
105 changed files with 14699 additions and 4630 deletions

View File

@@ -152,13 +152,87 @@ class SuppliersServiceClient(BaseServiceClient):
data = {"status": status}
result = await self.put(f"suppliers/purchase-orders/{order_id}/status", data=data, tenant_id=tenant_id)
if result:
logger.info("Updated purchase order status",
logger.info("Updated purchase order status",
order_id=order_id, status=status, tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error updating purchase order status",
logger.error("Error updating purchase order status",
error=str(e), order_id=order_id, tenant_id=tenant_id)
return None
async def approve_purchase_order(
self,
tenant_id: str,
po_id: str,
approval_data: Dict[str, Any]
) -> Optional[Dict[str, Any]]:
"""
Auto-approve a purchase order
Args:
tenant_id: Tenant ID
po_id: Purchase Order ID
approval_data: Approval data including:
- approved_by: User ID or "system" for auto-approval
- approval_notes: Notes about the approval
- auto_approved: Boolean flag indicating auto-approval
- approval_reasons: List of reasons for auto-approval
Returns:
Updated purchase order data or None
"""
try:
# Format the approval request payload
payload = {
"action": "approve",
"notes": approval_data.get("approval_notes", "Auto-approved by system")
}
result = await self.post(
f"suppliers/purchase-orders/{po_id}/approve",
data=payload,
tenant_id=tenant_id
)
if result:
logger.info("Auto-approved purchase order",
po_id=po_id,
tenant_id=tenant_id,
auto_approved=approval_data.get("auto_approved", True))
return result
except Exception as e:
logger.error("Error auto-approving purchase order",
error=str(e),
po_id=po_id,
tenant_id=tenant_id)
return None
async def get_supplier(self, tenant_id: str, supplier_id: str) -> Optional[Dict[str, Any]]:
"""
Get supplier details with performance metrics
Args:
tenant_id: Tenant ID
supplier_id: Supplier ID
Returns:
Supplier data including performance metrics or None
"""
try:
# Use the existing get_supplier_by_id method which returns full supplier data
result = await self.get_supplier_by_id(tenant_id, supplier_id)
if result:
logger.info("Retrieved supplier data for auto-approval",
supplier_id=supplier_id,
tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error getting supplier data",
error=str(e),
supplier_id=supplier_id,
tenant_id=tenant_id)
return None
# ================================================================
# DELIVERY MANAGEMENT

View File

@@ -184,7 +184,8 @@ class BaseServiceSettings(BaseSettings):
POS_SERVICE_URL: str = os.getenv("POS_SERVICE_URL", "http://pos-service:8000")
NOMINATIM_SERVICE_URL: str = os.getenv("NOMINATIM_SERVICE_URL", "http://nominatim:8080")
DEMO_SESSION_SERVICE_URL: str = os.getenv("DEMO_SESSION_SERVICE_URL", "http://demo-session-service:8000")
ALERT_PROCESSOR_SERVICE_URL: str = os.getenv("ALERT_PROCESSOR_SERVICE_URL", "http://alert-processor-api:8010")
# HTTP Client Settings
HTTP_TIMEOUT: int = int(os.getenv("HTTP_TIMEOUT", "30"))
HTTP_RETRIES: int = int(os.getenv("HTTP_RETRIES", "3"))
@@ -327,7 +328,34 @@ class BaseServiceSettings(BaseSettings):
ENABLE_SPANISH_HOLIDAYS: bool = os.getenv("ENABLE_SPANISH_HOLIDAYS", "true").lower() == "true"
ENABLE_MADRID_HOLIDAYS: bool = os.getenv("ENABLE_MADRID_HOLIDAYS", "true").lower() == "true"
SCHOOL_CALENDAR_ENABLED: bool = os.getenv("SCHOOL_CALENDAR_ENABLED", "true").lower() == "true"
# ================================================================
# PROCUREMENT AUTOMATION
# ================================================================
# Auto-PO Creation
AUTO_CREATE_POS_FROM_PLAN: bool = os.getenv("AUTO_CREATE_POS_FROM_PLAN", "true").lower() == "true"
AUTO_APPROVE_ENABLED: bool = os.getenv("AUTO_APPROVE_ENABLED", "true").lower() == "true"
AUTO_APPROVE_THRESHOLD_EUR: float = float(os.getenv("AUTO_APPROVE_THRESHOLD_EUR", "500.0"))
AUTO_APPROVE_TRUSTED_SUPPLIERS: bool = os.getenv("AUTO_APPROVE_TRUSTED_SUPPLIERS", "true").lower() == "true"
AUTO_APPROVE_MIN_SUPPLIER_SCORE: float = float(os.getenv("AUTO_APPROVE_MIN_SUPPLIER_SCORE", "0.80"))
# Approval Rules
REQUIRE_APPROVAL_ABOVE_EUR: float = float(os.getenv("REQUIRE_APPROVAL_ABOVE_EUR", "500.0"))
REQUIRE_APPROVAL_NEW_SUPPLIERS: bool = os.getenv("REQUIRE_APPROVAL_NEW_SUPPLIERS", "true").lower() == "true"
REQUIRE_APPROVAL_CRITICAL_ITEMS: bool = os.getenv("REQUIRE_APPROVAL_CRITICAL_ITEMS", "true").lower() == "true"
# Notifications
PO_APPROVAL_REMINDER_HOURS: int = int(os.getenv("PO_APPROVAL_REMINDER_HOURS", "24"))
PO_CRITICAL_ESCALATION_HOURS: int = int(os.getenv("PO_CRITICAL_ESCALATION_HOURS", "12"))
SEND_AUTO_APPROVAL_SUMMARY: bool = os.getenv("SEND_AUTO_APPROVAL_SUMMARY", "true").lower() == "true"
AUTO_APPROVAL_SUMMARY_TIME_HOUR: int = int(os.getenv("AUTO_APPROVAL_SUMMARY_TIME_HOUR", "18"))
# Procurement Planning
PROCUREMENT_PLANNING_ENABLED: bool = os.getenv("PROCUREMENT_PLANNING_ENABLED", "true").lower() == "true"
PROCUREMENT_PLAN_HORIZON_DAYS: int = int(os.getenv("PROCUREMENT_PLAN_HORIZON_DAYS", "14"))
PROCUREMENT_TEST_MODE: bool = os.getenv("PROCUREMENT_TEST_MODE", "false").lower() == "true"
# ================================================================
# DEVELOPMENT & TESTING
# ================================================================

View File

@@ -110,7 +110,10 @@ class BaseFastAPIService:
self.metrics_collector = setup_metrics_early(self.app, self.service_name)
# Setup distributed tracing
if self.enable_tracing:
# Check both constructor flag and environment variable
tracing_enabled = self.enable_tracing and os.getenv("ENABLE_TRACING", "true").lower() == "true"
if tracing_enabled:
try:
jaeger_endpoint = os.getenv(
"JAEGER_COLLECTOR_ENDPOINT",
@@ -120,6 +123,8 @@ class BaseFastAPIService:
self.logger.info(f"Distributed tracing enabled for {self.service_name}")
except Exception as e:
self.logger.warning(f"Failed to setup tracing, continuing without it: {e}")
else:
self.logger.info(f"Distributed tracing disabled for {self.service_name}")
# Setup lifespan
self.app.router.lifespan_context = self._create_lifespan()

View File

@@ -14,6 +14,33 @@ import structlog
logger = structlog.get_logger()
def format_quantity(value: float, decimals: int = 2) -> str:
"""
Format quantity with proper rounding to avoid floating point errors
Args:
value: The numeric value to format
decimals: Number of decimal places (default 2)
Returns:
Formatted string with proper decimal representation
"""
return f"{round(value, decimals):.{decimals}f}"
def format_currency(value: float) -> str:
"""
Format currency value with proper rounding
Args:
value: The currency value to format
Returns:
Formatted currency string
"""
return f"{round(value, 2):.2f}"
class AlertSeverity:
"""Alert severity levels"""
LOW = "low"
@@ -208,6 +235,9 @@ async def generate_inventory_alerts(
if days_until_expiry < 0:
# Expired stock
qty_formatted = format_quantity(float(stock.current_quantity))
loss_formatted = format_currency(float(stock.total_cost)) if stock.total_cost else "0.00"
await create_demo_alert(
db=db,
tenant_id=tenant_id,
@@ -215,7 +245,7 @@ async def generate_inventory_alerts(
severity=AlertSeverity.URGENT,
title=f"Stock Caducado: {ingredient.name}",
message=f"El lote {stock.batch_number} caducó hace {abs(days_until_expiry)} días. "
f"Cantidad: {stock.current_quantity:.2f} {ingredient.unit_of_measure.value}. "
f"Cantidad: {qty_formatted} {ingredient.unit_of_measure.value}. "
f"Acción requerida: Retirar inmediatamente del inventario y registrar como pérdida.",
service="inventory",
rabbitmq_client=rabbitmq_client,
@@ -225,15 +255,17 @@ async def generate_inventory_alerts(
"batch_number": stock.batch_number,
"expiration_date": stock.expiration_date.isoformat(),
"days_expired": abs(days_until_expiry),
"quantity": float(stock.current_quantity),
"quantity": qty_formatted,
"unit": ingredient.unit_of_measure.value,
"estimated_loss": float(stock.total_cost) if stock.total_cost else 0.0
"estimated_loss": loss_formatted
}
)
alerts_created += 1
elif days_until_expiry <= 3:
# Expiring soon
qty_formatted = format_quantity(float(stock.current_quantity))
await create_demo_alert(
db=db,
tenant_id=tenant_id,
@@ -241,7 +273,7 @@ async def generate_inventory_alerts(
severity=AlertSeverity.HIGH,
title=f"Próximo a Caducar: {ingredient.name}",
message=f"El lote {stock.batch_number} caduca en {days_until_expiry} día{'s' if days_until_expiry > 1 else ''}. "
f"Cantidad: {stock.current_quantity:.2f} {ingredient.unit_of_measure.value}. "
f"Cantidad: {qty_formatted} {ingredient.unit_of_measure.value}. "
f"Recomendación: Planificar uso prioritario en producción inmediata.",
service="inventory",
rabbitmq_client=rabbitmq_client,
@@ -251,7 +283,7 @@ async def generate_inventory_alerts(
"batch_number": stock.batch_number,
"expiration_date": stock.expiration_date.isoformat(),
"days_until_expiry": days_until_expiry,
"quantity": float(stock.current_quantity),
"quantity": qty_formatted,
"unit": ingredient.unit_of_measure.value
}
)
@@ -260,26 +292,31 @@ async def generate_inventory_alerts(
# Low stock alert
if stock.current_quantity < ingredient.low_stock_threshold:
shortage = ingredient.low_stock_threshold - stock.current_quantity
current_qty = format_quantity(float(stock.current_quantity))
threshold_qty = format_quantity(float(ingredient.low_stock_threshold))
shortage_qty = format_quantity(float(shortage))
reorder_qty = format_quantity(float(ingredient.reorder_quantity))
await create_demo_alert(
db=db,
tenant_id=tenant_id,
alert_type="low_stock",
severity=AlertSeverity.MEDIUM,
title=f"Stock Bajo: {ingredient.name}",
message=f"Stock actual: {stock.current_quantity:.2f} {ingredient.unit_of_measure.value}. "
f"Umbral mínimo: {ingredient.low_stock_threshold:.2f}. "
f"Faltante: {shortage:.2f} {ingredient.unit_of_measure.value}. "
f"Se recomienda realizar pedido de {ingredient.reorder_quantity:.2f} {ingredient.unit_of_measure.value}.",
message=f"Stock actual: {current_qty} {ingredient.unit_of_measure.value}. "
f"Umbral mínimo: {threshold_qty}. "
f"Faltante: {shortage_qty} {ingredient.unit_of_measure.value}. "
f"Se recomienda realizar pedido de {reorder_qty} {ingredient.unit_of_measure.value}.",
service="inventory",
rabbitmq_client=rabbitmq_client,
metadata={
"stock_id": str(stock.id),
"ingredient_id": str(ingredient.id),
"current_quantity": float(stock.current_quantity),
"threshold": float(ingredient.low_stock_threshold),
"reorder_point": float(ingredient.reorder_point),
"reorder_quantity": float(ingredient.reorder_quantity),
"shortage": float(shortage)
"current_quantity": current_qty,
"threshold": threshold_qty,
"reorder_point": format_quantity(float(ingredient.reorder_point)),
"reorder_quantity": reorder_qty,
"shortage": shortage_qty
}
)
alerts_created += 1
@@ -287,24 +324,28 @@ async def generate_inventory_alerts(
# Overstock alert (if max_stock_level is defined)
if ingredient.max_stock_level and stock.current_quantity > ingredient.max_stock_level:
excess = stock.current_quantity - ingredient.max_stock_level
current_qty = format_quantity(float(stock.current_quantity))
max_level_qty = format_quantity(float(ingredient.max_stock_level))
excess_qty = format_quantity(float(excess))
await create_demo_alert(
db=db,
tenant_id=tenant_id,
alert_type="overstock",
severity=AlertSeverity.LOW,
title=f"Exceso de Stock: {ingredient.name}",
message=f"Stock actual: {stock.current_quantity:.2f} {ingredient.unit_of_measure.value}. "
f"Nivel máximo recomendado: {ingredient.max_stock_level:.2f}. "
f"Exceso: {excess:.2f} {ingredient.unit_of_measure.value}. "
message=f"Stock actual: {current_qty} {ingredient.unit_of_measure.value}. "
f"Nivel máximo recomendado: {max_level_qty}. "
f"Exceso: {excess_qty} {ingredient.unit_of_measure.value}. "
f"Considerar reducir cantidad en próximos pedidos o buscar uso alternativo.",
service="inventory",
rabbitmq_client=rabbitmq_client,
metadata={
"stock_id": str(stock.id),
"ingredient_id": str(ingredient.id),
"current_quantity": float(stock.current_quantity),
"max_level": float(ingredient.max_stock_level),
"excess": float(excess)
"current_quantity": current_qty,
"max_level": max_level_qty,
"excess": excess_qty
}
)
alerts_created += 1
@@ -437,21 +478,23 @@ async def generate_equipment_alerts(
# Low efficiency alert
if equipment.efficiency_percentage and equipment.efficiency_percentage < 80.0:
efficiency_formatted = format_quantity(float(equipment.efficiency_percentage), 1)
await create_demo_alert(
db=db,
tenant_id=tenant_id,
alert_type="equipment_low_efficiency",
severity=AlertSeverity.LOW,
title=f"Eficiencia Baja: {equipment.name}",
message=f"El equipo {equipment.name} está operando con eficiencia reducida ({equipment.efficiency_percentage:.1f}%). "
f"Eficiencia objetivo: e 85%. "
message=f"El equipo {equipment.name} está operando con eficiencia reducida ({efficiency_formatted}%). "
f"Eficiencia objetivo: 85%. "
f"Revisar causas: limpieza, calibración, desgaste de componentes.",
service="production",
rabbitmq_client=rabbitmq_client,
metadata={
"equipment_id": str(equipment.id),
"equipment_name": equipment.name,
"efficiency": float(equipment.efficiency_percentage)
"efficiency": efficiency_formatted
}
)
alerts_created += 1
@@ -554,6 +597,8 @@ async def generate_order_alerts(
# High priority pending orders
if order.priority == 'high' and order.status == 'pending':
amount_formatted = format_currency(float(order.total_amount))
await create_demo_alert(
db=db,
tenant_id=tenant_id,
@@ -561,7 +606,7 @@ async def generate_order_alerts(
severity=AlertSeverity.MEDIUM,
title=f"Pedido Prioritario Pendiente: {order.order_number}",
message=f"El pedido de alta prioridad {order.order_number} está pendiente de confirmación. "
f"Monto: ¬{float(order.total_amount):.2f}. "
f"Monto: {amount_formatted}. "
f"Revisar disponibilidad de ingredientes y confirmar producción.",
service="orders",
rabbitmq_client=rabbitmq_client,
@@ -569,7 +614,7 @@ async def generate_order_alerts(
"order_id": str(order.id),
"order_number": order.order_number,
"priority": order.priority,
"total_amount": float(order.total_amount)
"total_amount": amount_formatted
}
)
alerts_created += 1