import sqlite3 from pathlib import Path from datetime import datetime from collections import Counter # ============================================================================ try: import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import matplotlib.ticker as mticker HAS_MPL = True except ImportError: HAS_MPL = False DB_PATH = Path(__file__).parent / "fallas_descarga.db" REPORTES_DIR = Path(__file__).parent / "reportes" REPORTES_DIR.mkdir(exist_ok=True) COLORS = { "Error HTTP 500": "#e74c3c", "falló: PDF": "#e67e22", "falló: XML": "#3498db", "falló: PDF, XML": "#9b59b6", } CMAP = list(COLORS.values()) # ============================================================================ def _query(sql, params=()): with sqlite3.connect(str(DB_PATH)) as conn: conn.row_factory = sqlite3.Row rows = conn.execute(sql, params).fetchall() return [dict(r) for r in rows] # ============================================================================ def reporte_texto(): print("\n" + "=" * 60) print("📊 REPORTE DE FALLAS - SUNAT") print("=" * 60) total = _query("SELECT COUNT(*) AS c FROM fallas")[0]["c"] print(f"\n📄 Total fallas registradas: {total}") if total == 0: print("\n✅ No hay fallas registradas.") return print("\n── Distribución por tipo de error ──") for r in _query("SELECT COUNT(*) AS c, error FROM fallas GROUP BY error ORDER BY c DESC"): pct = r["c"] / total * 100 print(f" {r['error']:25s} {r['c']:>5d} ({pct:5.1f}%)") print("\n── Top 10 RUC con más fallas ──") for i, r in enumerate(_query("SELECT COUNT(*) AS c, ruc FROM fallas GROUP BY ruc ORDER BY c DESC LIMIT 10"), 1): print(f" {i:2d}. {r['ruc']} ({r['c']} fallas)") print("\n── Distribución por intentos ──") for r in _query("SELECT intentos, COUNT(*) AS c FROM fallas GROUP BY intentos ORDER BY intentos"): print(f" {r['intentos']:2d} intento(s) → {r['c']:>5d} documento(s)") print("\n── Fallas por día ──") for r in _query("SELECT DATE(fecha_ultimo) AS dia, COUNT(*) AS c FROM fallas GROUP BY dia ORDER BY dia"): print(f" {r['dia']} → {r['c']} falla(s)") # ============================================================================ def _color_error(error): return COLORS.get(error, "#95a5a6") # ============================================================================ def grafico_barras_errores(): data = _query("SELECT COUNT(*) AS c, error FROM fallas GROUP BY error ORDER BY c DESC") if not data: return None labels = [r["error"] for r in data] values = [r["c"] for r in data] colors = [_color_error(l) for l in labels] fig, ax = plt.subplots(figsize=(10, 5)) bars = ax.barh(labels, values, color=colors, edgecolor="white", height=0.6) for b, v in zip(bars, values): ax.text(b.get_width() + max(values) * 0.01, b.get_y() + b.get_height() / 2, str(v), va="center", fontsize=11, fontweight="bold") ax.set_xlabel("Cantidad de fallas") ax.set_title("Distribución de fallas por tipo de error", fontsize=14, fontweight="bold") ax.margins(x=0.15) ax.spines[["top", "right"]].set_visible(False) fig.tight_layout() path = REPORTES_DIR / "barras_errores.png" fig.savefig(path, dpi=150) plt.close(fig) return path # ============================================================================ def grafico_barras_ruc(): data = _query("SELECT COUNT(*) AS c, ruc FROM fallas GROUP BY ruc ORDER BY c DESC LIMIT 10") if not data: return None data.reverse() labels = [r["ruc"] for r in data] values = [r["c"] for r in data] fig, ax = plt.subplots(figsize=(10, 5)) bars = ax.barh(labels, values, color="#e67e22", edgecolor="white", height=0.6) for b, v in zip(bars, values): ax.text(b.get_width() + 0.3, b.get_y() + b.get_height() / 2, str(v), va="center", fontsize=10, fontweight="bold") ax.set_xlabel("Cantidad de fallas") ax.set_title("Top 10 RUC con más fallas", fontsize=14, fontweight="bold") ax.margins(x=0.15) ax.spines[["top", "right"]].set_visible(False) fig.tight_layout() path = REPORTES_DIR / "barras_ruc.png" fig.savefig(path, dpi=150) plt.close(fig) return path # ============================================================================ def grafico_torta(): data = _query("SELECT COUNT(*) AS c, error FROM fallas GROUP BY error ORDER BY c DESC") if not data: return None labels = [r["error"] for r in data] values = [r["c"] for r in data] colors = [_color_error(l) for l in labels] fig, ax = plt.subplots(figsize=(7, 7)) wedges, texts, autotexts = ax.pie( values, labels=labels, autopct="%1.1f%%", colors=colors, startangle=90, textprops={"fontsize": 11} ) for at in autotexts: at.set_fontweight("bold") ax.set_title("Proporción de fallas por tipo", fontsize=14, fontweight="bold") fig.tight_layout() path = REPORTES_DIR / "torta_errores.png" fig.savefig(path, dpi=150) plt.close(fig) return path # ============================================================================ def main(): reporte_texto() if not HAS_MPL: print("\n⚠️ matplotlib no instalado. Ejecuta: pip3 install matplotlib") return paths = [] for name, fn in [("barras_errores", grafico_barras_errores), ("barras_ruc", grafico_barras_ruc), ("torta_errores", grafico_torta)]: p = fn() if p: paths.append(p) if paths: print(f"\n📁 Gráficos guardados en: {REPORTES_DIR}") for p in paths: print(f" {p.name}") print(f"\n💡 Abre con: open {REPORTES_DIR}") # ============================================================================ if __name__ == "__main__": main() # ============================================================================