Files
GeorgeandClaude Sonnet 5.5 f0ad6aa78e se valida si la factura ya existe en SAP y se avisa cuando SAP está fuera de línea
- sapb1_manager.buscar_factura_existente busca la factura (proveedor, tipo,
  serie, número) en facturas de compra no anuladas y en drafts; si existe no
  se crea otro draft y se responde "ya está registrada" sin pisar el registro
  de una aceptación anterior.
- SapNoDisponible (subclase de ConnectionError) distingue una caída (timeout,
  sin conexión, HTTP 5xx) de un rechazo normal de SAP (HTTP 4xx). Una caída
  de Service Layer con HANA sin resultados ya no se lee como "guía no existe".
- Con SAP o HANA caídos se avisa una sola vez al proveedor con la plantilla
  "En cola" y el correo se reintenta en los ciclos siguientes (hasta 24), sin
  repetir avisos; al agotarse se avisa al equipo.
- La lógica por factura pasa a _procesar_factura; lo ya resuelto dentro de un
  correo no se repite al reintentarlo.
- seguimiento: columna intentos (con migración), registrar_intento y el
  estado de notificación en_espera.
- database_utilities.DB_NAME pasa a ser absoluto (junto al código).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 12:08:38 -05:00

4.9 KiB

seguimiento.py — Códigos de seguimiento y resultado por comprobante

Cada comprobante que llega por correo recibe un código de seguimiento (ABR-AAMMDD-NNNNN, por ejemplo ABR-261019-00042) que se le comunicará al proveedor y que permite reconocer el comprobante si lo reenvía. El registro también guarda el resultado del proceso y el estado de la notificación.

Los datos viven en la tabla comprobantes de emails.db. El esquema lo crea database_utilities.init_db() con CREATE TABLE IF NOT EXISTS, por lo que una base existente se actualiza sola sin tocar la tabla emails.

Identidad

  • Un comprobante se identifica por (RUC emisor, tipo, serie, número). Si vuelve a llegar, en el mismo correo o en otro, conserva su código.
  • Un correo que no trae ningún comprobante identificable (sin adjuntos, sin XML válido…) se registra aparte con registrar_correo: una fila por correo, con RUC, serie y número en NULL.
  • El código sale del id de la fila (ABR- + fecha + id con 5 dígitos), así que es único por construcción, sin reintentos ni contadores aparte.

Tabla comprobantes

Columna Contenido
codigo_seguimiento Código único del comprobante
email_id Último correo en el que llegó
ruc_emisor, tipo, serie, numero Identidad del comprobante (NULL en las filas de correo sin comprobante)
estado pendiente, aceptado, rechazado o error
observaciones JSON con los motivos de rechazo o el detalle del error
doc_entry DocEntry del draft creado en SAP
veces_recibido Cuántos correos distintos trajeron este comprobante
notificacion_estado, notificacion_error, notificacion_fecha pendiente, enviada, fallida, omitida o en_espera (se avisó que el servicio está fuera de línea), y su motivo
intentos Veces que se intentó procesar el comprobante con SAP fuera de línea (tope: MAX_REINTENTOS)
notificacion_destinatario Direcciones a las que se envió de verdad (en modo prueba no son las del remitente)
creado_en, actualizado_en Marcas de tiempo

Funciones

Función Qué hace
registrar_comprobante(email_id, ruc_emisor, tipo, serie, numero) Crea el registro o recupera el existente. Devuelve la fila más reenvio (True si ya había llegado en otro correo). Reprocesar el mismo correo no cambia nada.
registrar_correo(email_id) Igual, para un correo sin comprobante. Idempotente por correo.
obtener(codigo) Devuelve el registro o None.
actualizar_resultado(codigo, estado, observaciones=None, doc_entry=None) Guarda el resultado. ValueError si el estado no es válido, LookupError si no existe el código.
registrar_notificacion(codigo, estado, error=None, destinatario=None) Guarda el estado de la notificación y a quién se envió. Mismas excepciones.
registrar_intento(codigo) Suma un intento fallido por SAP fuera de línea y devuelve el total.
contar_respuestas_correo(remitente, horas=24) Respuestas enviada a ese remitente en correos sin comprobante: el freno contra bucles de respuesta_proveedor.

Uso en el pipeline

En pipeline._procesar_un_correo, cada factura se registra al empezar (_registrar_comprobante) y su resultado se guarda al terminar (_registrar_resultado):

Resultado del proceso estado Detalle guardado
Draft creado en SAP aceptado doc_entry
Precios no coinciden con SAP rechazado [{"codigo": "precios_no_coinciden"}]
Error al crear el draft error [{"codigo": "error_sap", "detalle": "..."}]

init_db agrega las columnas notificacion_destinatario e intentos a una tabla creada antes sin ellas, sin tocar los datos.

El seguimiento nunca impide procesar la factura: si falla la base de datos, se registra un WARNING y el flujo continúa. Si el comprobante ya había llegado antes, se registra un WARNING con las veces recibidas y el estado anterior, pero no se bloquea la creación de otro draft.

Observaciones guardadas

  • Rechazo de una factura: la lista de observaciones completa (codigo, titulo, filas, como_corregir; ver observaciones.py).
  • Error: [{"codigo": "error_interno", "detalle": "..."}].
  • SAP fuera de línea: [{"codigo": "servicio_no_disponible", "detalle": "...", "intentos": n}].
  • Correo sin comprobante: la observación del motivo (sin_pdf_xml, falta_xml, sin_factura).

Las notificaciones las registra respuesta_proveedor (enviada, fallida u omitida con su motivo).

Notas

  • database_utilities.DB_NAME es emails.db junto al código (ruta absoluta): no depende del directorio desde el que se lance el demonio, por ejemplo como servicio de Windows.
  • Las pruebas redirigen DB_NAME a una base temporal (fixture automático en tests/conftest.py) para no escribir en emails.db.