168 lines
6.0 KiB
Python
168 lines
6.0 KiB
Python
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()
|
|
# ============================================================================ |