Improve the frontend
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
# ================================================================
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user