La comparación abs(a - b) < 0.01 no era fiable por la coma flotante: el mismo céntimo se aceptaba o rechazaba según el importe (431.41 contra 431.40 se rechazaba y 624.39 contra 624.38 se aceptaba). pipeline_utils.importes_coinciden redondea la diferencia a 2 decimales, lo que muestra un comprobante, y admite hasta 1 céntimo. Se usa en la validación de precios y en la comparación del valor referencial. Con datos reales de SAP (49 líneas) ningún resultado cambió. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
124 lines
5.4 KiB
Python
124 lines
5.4 KiB
Python
"""Cuerpos HTML de los correos de notificación."""
|
|
|
|
from pipeline_utils import importes_coinciden
|
|
|
|
|
|
|
|
|
|
# ============================================================================
|
|
def build_validation_html(resultados: list[dict]) -> str:
|
|
"""
|
|
Genera una tabla HTML con el resultado de la validación de precios
|
|
para incluir en el cuerpo del email de notificación.
|
|
|
|
Args:
|
|
resultados: Lista de resultados de ``validate_prices``.
|
|
|
|
Returns:
|
|
str — Código HTML de la tabla con columnas:
|
|
Guía, Estado, Draft Price, SAP, Almacén, Resultado.
|
|
|
|
Example:
|
|
Con un resultado ``ok=True`` y ``grflete="09T001-127773"``::
|
|
|
|
html = build_validation_html(resultados)
|
|
# → "<table ...><tr><td>09T001-127773</td><td>✅</td>...</tr></table>"
|
|
"""
|
|
rows = ""
|
|
for r in resultados:
|
|
icono = "✅" if r["ok"] else "❌"
|
|
dp = f"{r['draft_price']:.6f}" if r["draft_price"] is not None else "-"
|
|
sap_price_str = f"{r['sap_price']:.6f}" if r["sap_price"] is not None else "-"
|
|
rows += f"<tr><td>{r['grflete']}</td><td>{icono}</td><td>{dp}</td><td>{sap_price_str}</td><td>{r['warehouse_code']}</td><td>{r['label']}</td></tr>"
|
|
return (
|
|
"<table border='1' cellpadding='6' cellspacing='0' style='border-collapse:collapse;'>"
|
|
"<tr><th>Guía</th><th>Estado</th><th>Draft Price</th><th>SAP</th><th>Almacén</th><th>Resultado</th></tr>"
|
|
f"{rows}</table>"
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
def build_valor_referencial_html(factura: dict, guias_valor_ref: list[dict]) -> str:
|
|
"""
|
|
Genera una tabla HTML comparando el valor referencial del XML de la
|
|
factura contra el valor referencial obtenido en SAP
|
|
(``Sp_ObtenerValorReferencial``) por cada guía de remisión, y el
|
|
total acumulado.
|
|
|
|
Args:
|
|
factura: Diccionario de la factura (usa ``lineas[]`` para sumar
|
|
``valor_referencial`` extraído del XML).
|
|
guias_valor_ref: Resultado de ``obtener_valor_referencial_sap``.
|
|
|
|
Returns:
|
|
str — Tabla HTML con una fila por guía, el total SAP, el valor
|
|
del XML y un ícono de coincidencia.
|
|
"""
|
|
valor_xml = sum(l.get("valor_referencial") or 0 for l in factura.get("lineas", []))
|
|
total_sap = sum(g["valor_sap"] for g in guias_valor_ref)
|
|
|
|
rows = "".join(
|
|
f"<tr><td>09{g['serie']}-{g['numero']}</td><td>{g['valor_sap']:.2f}</td></tr>"
|
|
for g in guias_valor_ref
|
|
)
|
|
coincide = importes_coinciden(valor_xml, total_sap)
|
|
icono = "✅" if coincide else "❌"
|
|
|
|
return (
|
|
"<table border='1' cellpadding='6' cellspacing='0' style='border-collapse:collapse;'>"
|
|
"<tr><th>Guía de Remisión</th><th>Valor Referencial SAP</th></tr>"
|
|
f"{rows}"
|
|
f"<tr><td><b>Total SAP</b></td><td><b>{total_sap:.2f}</b></td></tr>"
|
|
f"<tr><td><b>Valor Referencial XML</b></td><td><b>{valor_xml:.2f}</b></td></tr>"
|
|
f"<tr><td colspan='2'>{icono} {'Coincide' if coincide else 'No coincide'}</td></tr>"
|
|
"</table>"
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
def build_notification_body(status: str, factura: dict, validation_table: str, valor_referencial_table: str, **kwargs) -> str:
|
|
"""
|
|
Construye el cuerpo HTML del email de notificación del resultado
|
|
del proceso SAP.
|
|
|
|
Args:
|
|
status: ``"created"`` (draft creado), ``"duplicate"`` (la factura ya
|
|
existe en SAP), ``"error"`` (error al
|
|
crear), o cualquier otro valor (validación falló, draft no
|
|
creado).
|
|
factura: Diccionario de la factura (se usa ``serie``,
|
|
``numero``, ``razon_social_emisor``).
|
|
validation_table: Tabla HTML generada por
|
|
``build_validation_html``.
|
|
valor_referencial_table: Tabla HTML generada por
|
|
``build_valor_referencial_html``.
|
|
**kwargs: Argumentos adicionales:
|
|
- ``doc_entry`` (int): DocEntry del draft creado.
|
|
- ``error`` (str): Mensaje de error si ocurrió.
|
|
|
|
Returns:
|
|
str — Cuerpo HTML completo del email.
|
|
|
|
Example:
|
|
Draft creado exitosamente::
|
|
|
|
body = build_notification_body(
|
|
"created", factura, tabla_html, valref_html, doc_entry=2021
|
|
)
|
|
# → "<p>✅ Draft SAP B1 creado exitosamente.</p>..."
|
|
"""
|
|
base = (
|
|
f"<p><b>Factura:</b> {factura.get('serie')}-{factura.get('numero')}</p>"
|
|
f"<p><b>Emisor:</b> {factura.get('razon_social_emisor', '')}</p>"
|
|
)
|
|
valref_section = f"<h3>Valor Referencial (XML vs SAP)</h3>{valor_referencial_table}"
|
|
if status == "created":
|
|
return f"<p>✅ Draft SAP B1 creado exitosamente.</p><p><b>DocEntry:</b> {kwargs.get('doc_entry')}</p>{base}<h3>Validación de precios</h3>{validation_table}{valref_section}"
|
|
elif status == "duplicate":
|
|
existente = kwargs.get("existente") or {}
|
|
return f"<p>⏭️ Draft NO creado: la factura ya existe en SAP ({existente.get('entidad')} {existente.get('DocEntry')}).</p>{base}"
|
|
elif status == "error":
|
|
return f"<p>❌ Error al crear Draft SAP B1.</p>{base}<p><b>Error:</b> {kwargs.get('error')}</p><h3>Validación de precios</h3>{validation_table}{valref_section}"
|
|
else:
|
|
return f"<p>⏭️ Draft NO creado. Los precios de las guías no coinciden con SAP.</p>{base}<h3>Validación de precios</h3>{validation_table}{valref_section}"
|