Spaces:
Sleeping
Sleeping
File size: 6,796 Bytes
6cc3191 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | import io
import json
import os
import tempfile
from pathlib import Path
import pandas as pd
import plotly.express as px
import streamlit as st
from dotenv import load_dotenv
from extractor import Invoice, extract_invoice_data
load_dotenv()
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.1-8b-instant")
SAMPLE_DATA = Path(__file__).parent / "sample_data.json"
st.set_page_config(page_title="Procesador de Facturas IA", page_icon="🧾", layout="wide")
st.title("🧾 Procesador de Facturas con IA")
st.caption("Sube tus facturas en PDF y obtén un dashboard completo en segundos.")
# ── Sidebar ──────────────────────────────────────────────────────────────────
with st.sidebar:
st.header("⚙️ Configuración")
model = st.selectbox("Modelo Groq", ["llama-3.1-8b-instant", "llama-3.3-70b-versatile"], index=0)
st.divider()
uploaded_files = st.file_uploader(
"📂 Sube tus facturas (PDF)",
type=["pdf"],
accept_multiple_files=True,
)
process_btn = st.button("🚀 Procesar Facturas", use_container_width=True, type="primary",
disabled=not uploaded_files)
st.divider()
if st.button("🔄 Cargar datos de ejemplo", use_container_width=True):
st.session_state.pop("invoices_df", None)
st.session_state["use_sample"] = True
st.rerun()
# ── Load sample data on first visit ──────────────────────────────────────────
if "invoices_df" not in st.session_state and not uploaded_files:
if SAMPLE_DATA.exists():
records = json.loads(SAMPLE_DATA.read_text(encoding="utf-8"))
st.session_state["invoices_df"] = pd.DataFrame(records)
st.session_state["is_sample"] = True
# ── Process uploaded files ────────────────────────────────────────────────────
if process_btn and uploaded_files:
if not GROQ_API_KEY:
st.error("No se encontró GROQ_API_KEY. Añádela al fichero .env", icon="🔑")
st.stop()
from groq import Groq
client = Groq(api_key=GROQ_API_KEY)
invoices: list[Invoice] = []
progress = st.progress(0, text="Procesando facturas...")
for i, uploaded_file in enumerate(uploaded_files):
progress.progress((i + 1) / len(uploaded_files), text=f"Procesando {uploaded_file.name}...")
tmp_path = Path(tempfile.gettempdir()) / uploaded_file.name
tmp_path.write_bytes(uploaded_file.read())
invoice = extract_invoice_data(str(tmp_path), client, GROQ_MODEL)
invoices.append(invoice)
tmp_path.unlink(missing_ok=True)
progress.empty()
df = pd.DataFrame([inv.model_dump() for inv in invoices])
df.insert(0, "archivo", [f.name for f in uploaded_files])
st.session_state["invoices_df"] = df
st.session_state["is_sample"] = False
st.success(f"✅ {len(invoices)} facturas procesadas correctamente.")
# ── Dashboard ─────────────────────────────────────────────────────────────────
if "invoices_df" not in st.session_state:
st.stop()
df: pd.DataFrame = st.session_state["invoices_df"]
if st.session_state.get("is_sample"):
st.info("Mostrando datos de ejemplo. Sube tus propias facturas PDF para analizarlas.", icon="📊")
# ── KPI Cards ──
col1, col2, col3, col4 = st.columns(4)
col1.metric("💶 Gasto Total", f"{df['total'].sum():,.2f} €")
col2.metric("🏦 IVA Total", f"{df['iva_importe'].sum():,.2f} €")
col3.metric("📄 Facturas", len(df))
col4.metric("🏢 Proveedores", df["proveedor"].nunique())
st.divider()
# ── Charts ──
col_left, col_right = st.columns(2)
with col_left:
st.subheader("Gasto por Categoría")
cat_df = df.groupby("categoria")["total"].sum().reset_index()
fig_cat = px.pie(cat_df, values="total", names="categoria", hole=0.4,
color_discrete_sequence=px.colors.qualitative.Set3)
fig_cat.update_traces(textposition="inside", textinfo="percent+label")
st.plotly_chart(fig_cat, use_container_width=True)
with col_right:
st.subheader("Gasto por Proveedor")
prov_df = df.groupby("proveedor")["total"].sum().reset_index().sort_values("total", ascending=True)
fig_prov = px.bar(prov_df, x="total", y="proveedor", orientation="h",
color="total", color_continuous_scale="Blues",
labels={"total": "Importe (€)", "proveedor": ""})
fig_prov.update_layout(coloraxis_showscale=False)
st.plotly_chart(fig_prov, use_container_width=True)
# Timeline chart
df_dates = df.dropna(subset=["fecha"]).copy()
if not df_dates.empty:
st.subheader("Evolución de Gastos por Fecha")
df_dates["fecha"] = pd.to_datetime(df_dates["fecha"], errors="coerce")
df_dates = df_dates.dropna(subset=["fecha"]).sort_values("fecha")
fig_time = px.bar(df_dates, x="fecha", y="total", color="categoria",
labels={"total": "Importe (€)", "fecha": "Fecha"},
color_discrete_sequence=px.colors.qualitative.Set2)
st.plotly_chart(fig_time, use_container_width=True)
# ── Data Table ──
st.subheader("📋 Detalle de Facturas")
display_cols = ["archivo", "proveedor", "numero_factura", "fecha", "concepto",
"categoria", "subtotal", "iva_porcentaje", "iva_importe", "total", "moneda"]
st.dataframe(
df[display_cols].style.format({
"subtotal": "{:.2f}",
"iva_importe": "{:.2f}",
"total": "{:.2f}",
"iva_porcentaje": "{:.1f}%",
}, na_rep="—"),
use_container_width=True,
hide_index=True,
)
# ── Export ──
st.subheader("⬇️ Exportar")
col_e1, col_e2 = st.columns(2)
with col_e1:
csv = df.to_csv(index=False).encode("utf-8")
st.download_button("📥 Descargar CSV", csv, "facturas.csv", "text/csv", use_container_width=True)
with col_e2:
excel_buf = io.BytesIO()
with pd.ExcelWriter(excel_buf, engine="openpyxl") as writer:
df.to_excel(writer, index=False, sheet_name="Facturas")
cat_df.to_excel(writer, index=False, sheet_name="Por Categoría")
prov_df.to_excel(writer, index=False, sheet_name="Por Proveedor")
st.download_button(
"📊 Descargar Excel",
excel_buf.getvalue(),
"facturas.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
use_container_width=True,
)
|