se responde al remitente con las plantillas de aceptado/rechazado

El sistema contesta a quien envió el correo con la plantilla que corresponda.
Por defecto (NOTIFICACION_MODO=prueba) la respuesta va solo al equipo con el
asunto "[PRUEBA → remitente]"; en modo real va al remitente con copia oculta
al equipo. El correo técnico (tablas de SAP, JSON) pasa a ir solo al equipo.

- respuesta_proveedor.py: contextos de las plantillas, envío y protecciones
  (remitente desconocido o sin dominio, propio buzón, no-reply, mensajes
  automáticos, respuestas a notificaciones, límite de 3 por remitente/24 h).
- observaciones.py: motivos de rechazo con título, filas y cómo corregir
  (borrador de textos); también para correos sin comprobante válido.
- Un error de SAP/HANA o un fallo inesperado no se le comunica al proveedor y
  ya no tumba el ciclo; "guía no encontrada" exige que HANA responda.
- outlook_utilities: es_mensaje_automatico y copia oculta en send_email.
- seguimiento: notificacion_destinatario (con migración) y
  contar_respuestas_correo.
- config_validator anuncia el modo activo; documentación y pruebas.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-22 08:55:54 -05:00
1 parent af9e3961e5
commit 4deb9d4e4d
22 files changed
+1660 -146

No files matched your search

+23 -4
View File
@@ -18,7 +18,7 @@ El esquema se crea en ``database_utilities.init_db``.
import json
import sqlite3
from contextlib import contextmanager
from datetime import datetime
from datetime import datetime, timedelta
import database_utilities
@@ -159,7 +159,8 @@ def actualizar_resultado(codigo_seguimiento: str, estado: str, observaciones: li
raise LookupError(f"No existe el código de seguimiento {codigo_seguimiento}")
def registrar_notificacion(codigo_seguimiento: str, estado: str, error: str | None = None) -> None:
def registrar_notificacion(codigo_seguimiento: str, estado: str, error: str | None = None,
destinatario: str | None = None) -> None:
"""
Guarda el estado de la notificación enviada al proveedor.
@@ -167,6 +168,7 @@ def registrar_notificacion(codigo_seguimiento: str, estado: str, error: str | No
codigo_seguimiento: Código del registro.
estado: Uno de ``ESTADOS_NOTIFICACION``.
error: Motivo, si la notificación falló o se omitió.
destinatario: Direcciones a las que se envió realmente (en modo prueba no son las del remitente).
Raises:
ValueError: Si ``estado`` no es válido.
@@ -179,9 +181,26 @@ def registrar_notificacion(codigo_seguimiento: str, estado: str, error: str | No
with _conexion() as conn:
cursor = conn.execute(
"""UPDATE comprobantes
SET notificacion_estado = ?, notificacion_error = ?, notificacion_fecha = ?, actualizado_en = ?
SET notificacion_estado = ?, notificacion_error = ?, notificacion_fecha = ?,
notificacion_destinatario = ?, actualizado_en = ?
WHERE codigo_seguimiento = ?""",
(estado, error, ahora, ahora, codigo_seguimiento),
(estado, error, ahora, destinatario, ahora, codigo_seguimiento),
)
if cursor.rowcount == 0:
raise LookupError(f"No existe el código de seguimiento {codigo_seguimiento}")
def contar_respuestas_correo(remitente: str, horas: int = 24) -> int:
"""
Cuenta las respuestas ya enviadas en las últimas ``horas`` a un remitente por
correos **sin comprobante** (sin serie ni número). Sirve de freno contra bucles
y contra remitentes que insisten con correos inválidos.
"""
desde = (datetime.now() - timedelta(hours=horas)).strftime("%Y-%m-%d %H:%M:%S")
with _conexion() as conn:
return conn.execute(
"""SELECT COUNT(*) FROM comprobantes c JOIN emails e ON e.id = c.email_id
WHERE c.serie IS NULL AND c.notificacion_estado = 'enviada'
AND c.notificacion_fecha >= ? AND lower(e.sender) = lower(?)""",
(desde, remitente),
).fetchone()[0]