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>
156 lines
5.4 KiB
Python
156 lines
5.4 KiB
Python
import sqlite3
|
|
|
|
# ============================================================================
|
|
# CONFIGURACIÓN
|
|
# ============================================================================
|
|
|
|
DB_NAME = "emails.db"
|
|
|
|
# ============================================================================
|
|
def init_db() -> None:
|
|
"""
|
|
Inicializa la base de datos SQLite y crea las tablas si no existen:
|
|
``emails`` (correos ya procesados) y ``comprobantes`` (códigos de
|
|
seguimiento y resultado de cada comprobante, ver ``seguimiento.py``).
|
|
"""
|
|
conn = sqlite3.connect(DB_NAME)
|
|
try:
|
|
cursor = conn.cursor()
|
|
|
|
# Crear tabla con id como llave primaria para evitar duplicados
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS emails (
|
|
id TEXT PRIMARY KEY,
|
|
fecha TEXT,
|
|
sender TEXT,
|
|
subject TEXT,
|
|
has_attach INTEGER,
|
|
fecha_descarga TEXT
|
|
)
|
|
''')
|
|
|
|
# Un comprobante se identifica por (ruc_emisor, tipo, serie, numero). Los
|
|
# correos sin comprobante identificable llevan serie NULL y son únicos por correo.
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS comprobantes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
codigo_seguimiento TEXT UNIQUE,
|
|
email_id TEXT NOT NULL,
|
|
ruc_emisor TEXT,
|
|
tipo TEXT,
|
|
serie TEXT,
|
|
numero TEXT,
|
|
estado TEXT NOT NULL DEFAULT 'pendiente',
|
|
observaciones TEXT,
|
|
doc_entry INTEGER,
|
|
veces_recibido INTEGER NOT NULL DEFAULT 1,
|
|
notificacion_estado TEXT NOT NULL DEFAULT 'pendiente',
|
|
notificacion_error TEXT,
|
|
notificacion_fecha TEXT,
|
|
notificacion_destinatario TEXT,
|
|
creado_en TEXT NOT NULL,
|
|
actualizado_en TEXT NOT NULL
|
|
)
|
|
''')
|
|
# Bases creadas antes de agregar una columna: se añade sin tocar los datos.
|
|
columnas = {fila[1] for fila in cursor.execute("PRAGMA table_info(comprobantes)")}
|
|
if "notificacion_destinatario" not in columnas:
|
|
cursor.execute("ALTER TABLE comprobantes ADD COLUMN notificacion_destinatario TEXT")
|
|
|
|
cursor.execute('''
|
|
CREATE UNIQUE INDEX IF NOT EXISTS ux_comprobantes_documento
|
|
ON comprobantes (ruc_emisor, tipo, serie, numero) WHERE serie IS NOT NULL
|
|
''')
|
|
cursor.execute('''
|
|
CREATE UNIQUE INDEX IF NOT EXISTS ux_comprobantes_correo
|
|
ON comprobantes (email_id) WHERE serie IS NULL
|
|
''')
|
|
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
# ============================================================================
|
|
def save_emails(emails_list: list[dict]) -> int:
|
|
"""
|
|
Guarda una lista de correos en la base de datos.
|
|
Retorna la cantidad de registros nuevos insertados.
|
|
"""
|
|
if not emails_list:
|
|
return 0
|
|
|
|
conn = sqlite3.connect(DB_NAME)
|
|
try:
|
|
cursor = conn.cursor()
|
|
|
|
from datetime import datetime
|
|
fecha_ahora = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
# Mapear lista de diccionarios a tuplas para executemany
|
|
# has_attach se guarda como 1 (True) o 0 (False)
|
|
data = [
|
|
(
|
|
e['id'],
|
|
e['fecha'],
|
|
e['sender'],
|
|
e['subject'],
|
|
1 if e['has_attach'] else 0,
|
|
fecha_ahora
|
|
)
|
|
for e in emails_list
|
|
]
|
|
|
|
# INSERT OR IGNORE evita errores si el ID ya existe
|
|
cursor.executemany('''
|
|
INSERT OR IGNORE INTO emails (id, fecha, sender, subject, has_attach, fecha_descarga)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
''', data)
|
|
|
|
conn.commit()
|
|
return cursor.rowcount
|
|
finally:
|
|
conn.close()
|
|
|
|
# ============================================================================
|
|
def get_existing_ids(ids_list: list[str]) -> list[str]:
|
|
"""
|
|
Recibe una lista de IDs y devuelve una lista con aquellos que ya existen en la BD.
|
|
"""
|
|
if not ids_list:
|
|
return []
|
|
|
|
conn = sqlite3.connect(DB_NAME)
|
|
try:
|
|
cursor = conn.cursor()
|
|
|
|
existing_ids = []
|
|
# Procesar por lotes para evitar límites de parámetros en SQLite
|
|
for i in range(0, len(ids_list), 900):
|
|
chunk = ids_list[i:i + 900]
|
|
placeholders = ','.join(['?'] * len(chunk))
|
|
query = f"SELECT id FROM emails WHERE id IN ({placeholders})"
|
|
cursor.execute(query, chunk)
|
|
existing_ids.extend([row[0] for row in cursor.fetchall()])
|
|
|
|
return existing_ids
|
|
finally:
|
|
conn.close()
|
|
|
|
# ============================================================================
|
|
def delete_email_by_id(email_id: str) -> bool:
|
|
"""
|
|
Elimina un correo de la base de datos por su ID.
|
|
Retorna True si se eliminó el registro, False si no se encontró.
|
|
"""
|
|
if not email_id:
|
|
return False
|
|
|
|
conn = sqlite3.connect(DB_NAME)
|
|
try:
|
|
cursor = conn.cursor()
|
|
cursor.execute("DELETE FROM emails WHERE id = ?", (email_id,))
|
|
conn.commit()
|
|
return cursor.rowcount > 0
|
|
finally:
|
|
conn.close()
|
|
# ============================================================================ |