Files
descarga_xml_sunat/gestor_fallas.py
T
GeorgeandClaude Sonnet 5 c2503745bf Reemplaza logging por consola/archivo, agrega historial de intentos y soporte para notas de crédito
- Nuevo logger_manager.py (reemplaza logger_utilities.py): logging con formato de árbol,
  rotación diaria a sunat.log, y consola por stdout (antes iba por stderr y se duplicaba
  en logs/error.log del launchd).
- Nuevo historial_manager.py: registra cada intento de consulta/descarga en historial.db
  para auditoría, con limpieza automática de registros de más de 180 días.
- consulta_cpe.py: corrige el id de notas de crédito/débito (usa F7/F8, no 07/08) y
  agrega reintentos ante errores 5xx.
- hana_manager.py: suma consulta a ORPC para notas de crédito (Indicator 07) y soporte
  de ObjType para actualizar la tabla correcta (OPCH/ORPC) al marcar PDF/XML descargado.
- gestor_fallas.py / main.py: propagan objtype end-to-end y dejan constancia de cada
  intento en historial_manager.
- Elimina login_api.py y reporte_fallas.py (código no usado / script manual sin uso reciente).
- Actualiza .gitignore (sunat-venv/, historial.db*) y docs/ para reflejar el estado real
  del proyecto.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 12:31:48 -05:00

93 lines
4.0 KiB
Python

import sqlite3
from pathlib import Path
from datetime import datetime, timedelta
DB_PATH = Path(__file__).parent / "fallas_descarga.db"
# ============================================================================
def _conn():
return sqlite3.connect(str(DB_PATH))
# ============================================================================
def init_db():
with _conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS fallas (
docentry INTEGER PRIMARY KEY,
ruc TEXT NOT NULL,
tipo TEXT NOT NULL,
serie TEXT NOT NULL,
numero TEXT NOT NULL,
objtype INTEGER NOT NULL DEFAULT 18,
error TEXT NOT NULL,
intentos INTEGER NOT NULL DEFAULT 1,
proximo_intento TEXT,
fecha_ultimo TEXT NOT NULL
)
""")
columnas = {row[1] for row in conn.execute("PRAGMA table_info(fallas)")}
if "obj_type" in columnas and "objtype" not in columnas:
conn.execute("ALTER TABLE fallas RENAME COLUMN obj_type TO objtype")
elif "objtype" not in columnas:
conn.execute("ALTER TABLE fallas ADD COLUMN objtype INTEGER NOT NULL DEFAULT 18")
# ============================================================================
def _calcular_proximo_intento(intentos):
if intentos >= 20:
return None
if intentos == 1:
delta = timedelta(minutes=0)
elif intentos == 2:
delta = timedelta(minutes=5)
elif intentos == 3:
delta = timedelta(minutes=15)
elif intentos == 4:
delta = timedelta(minutes=30)
elif intentos == 5:
delta = timedelta(hours=1)
elif intentos <= 10:
delta = timedelta(hours=2)
elif intentos <= 15:
delta = timedelta(hours=6)
else:
delta = timedelta(hours=12)
return (datetime.now() + delta).isoformat()
# ============================================================================
def registrar_falla(docentry, ruc, tipo_cpe, serie, numero, obj_type, error, intentos=None):
ahora = datetime.now().isoformat()
if intentos is None:
with _conn() as conn:
row = conn.execute(
"SELECT intentos FROM fallas WHERE docentry = ?", (docentry,)
).fetchone()
intentos = (row[0] + 1) if row else 1
proximo = _calcular_proximo_intento(intentos)
with _conn() as conn:
conn.execute("""
INSERT OR REPLACE INTO fallas
(docentry, ruc, tipo, serie, numero, objtype, error, intentos, proximo_intento, fecha_ultimo)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (docentry, ruc, tipo_cpe, serie, numero, obj_type, str(error)[:200], intentos, proximo, ahora))
# ============================================================================
def eliminar_falla(docentry):
with _conn() as conn:
conn.execute("DELETE FROM fallas WHERE docentry = ?", (docentry,))
# ============================================================================
def obtener_fallas_pendientes() -> list[dict]:
ahora = datetime.now().isoformat()
with _conn() as conn:
conn.row_factory = sqlite3.Row
cursor = conn.execute("""
SELECT * FROM fallas
WHERE (proximo_intento IS NULL OR proximo_intento <= ?)
AND intentos < 20
ORDER BY intentos ASC, fecha_ultimo ASC
""", (ahora,))
return [dict(row) for row in cursor.fetchall()]
# ============================================================================
def obtener_docentries_falla() -> set[int]:
with _conn() as conn:
cursor = conn.execute("SELECT docentry FROM fallas")
return {row[0] for row in cursor.fetchall()}
# ============================================================================
def total_fallas() -> int:
with _conn() as conn:
return conn.execute("SELECT COUNT(*) FROM fallas").fetchone()[0]
# ============================================================================