- 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>
164 lines
5.8 KiB
Python
164 lines
5.8 KiB
Python
import os
|
|
import sqlite3
|
|
|
|
# ============================================================================
|
|
# CONFIGURACIÓN
|
|
# ============================================================================
|
|
|
|
# Junto al código, no relativa al directorio de trabajo: el demonio (por ejemplo como servicio de
|
|
# Windows) puede lanzarse desde cualquier carpeta y debe encontrar siempre la misma base.
|
|
DB_NAME = os.path.join(os.path.dirname(os.path.abspath(__file__)), "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,
|
|
intentos INTEGER NOT NULL DEFAULT 0,
|
|
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)")}
|
|
for columna, definicion in (
|
|
("notificacion_destinatario", "TEXT"),
|
|
("intentos", "INTEGER NOT NULL DEFAULT 0"),
|
|
):
|
|
if columna not in columnas:
|
|
cursor.execute(f"ALTER TABLE comprobantes ADD COLUMN {columna} {definicion}")
|
|
|
|
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()
|
|
# ============================================================================ |