- 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>
93 lines
4.0 KiB
Python
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]
|
|
# ============================================================================ |