diff --git a/app.py b/app.py index cb104f6..742d61e 100644 --- a/app.py +++ b/app.py @@ -29,9 +29,17 @@ from werkzeug.security import generate_password_hash, check_password_hash # Configuracion # ----------------------------------------------------------------------- BASE_DIR = Path(__file__).parent -DATOS_JSON = BASE_DIR / "datos.json" USERS_FILE = BASE_DIR / "users.json" -TICKETS_DIR = BASE_DIR / "tickets" + +def get_tickets_dir(usuario: str) -> Path: + """Directorio de tickets del usuario. Se crea si no existe.""" + d = BASE_DIR / "tickets" / usuario + d.mkdir(parents=True, exist_ok=True) + return d + +def get_datos_json(usuario: str) -> Path: + """Ruta al datos.json del usuario.""" + return BASE_DIR / "datos" / usuario / "datos.json" ALLOWED_IMAGE_TYPES = {'image/jpeg', 'image/jpg', 'image/png', 'image/webp'} MAX_IMAGE_BYTES = 15 * 1024 * 1024 # 15 MB @@ -195,6 +203,9 @@ def registro(): "nombre": nombre } guardar_usuarios(users) + # Crear carpeta de tickets para el nuevo usuario + (BASE_DIR / "tickets" / usuario).mkdir(parents=True, exist_ok=True) + (BASE_DIR / "datos" / usuario).mkdir(parents=True, exist_ok=True) ok = "Cuenta creada correctamente. Ya puedes iniciar sesion." return render_template("registro.html", error=error, ok=ok) @@ -212,9 +223,10 @@ def index(): @app.route("/api/datos") @login_required def api_datos(): - if not DATOS_JSON.exists(): + datos_json = get_datos_json(session["usuario"]) + if not datos_json.exists(): return jsonify({"error": "datos.json no generado", "predicciones": []}), 404 - with open(DATOS_JSON, encoding="utf-8") as f: + with open(datos_json, encoding="utf-8") as f: datos = json.load(f) return jsonify(datos) @@ -224,13 +236,14 @@ def api_datos(): @app.route("/api/regenerar", methods=["POST"]) @login_required def api_regenerar(): + usuario = session["usuario"] try: subprocess.run( - [sys.executable, str(BASE_DIR / "autocompra7.py")], + [sys.executable, str(BASE_DIR / "autocompra7.py"), "--usuario", usuario], cwd=str(BASE_DIR), check=True, capture_output=True, timeout=120 ) subprocess.run( - [sys.executable, str(BASE_DIR / "generar_lista.py")], + [sys.executable, str(BASE_DIR / "generar_lista.py"), "--usuario", usuario], cwd=str(BASE_DIR), check=True, capture_output=True, timeout=30 ) return jsonify({"ok": True, "mensaje": "Pipeline ejecutado correctamente"}) @@ -245,7 +258,8 @@ def api_regenerar(): @app.route("/tickets/") @login_required def ticket_file(filename): - return send_from_directory(str(TICKETS_DIR), filename) + # Cada usuario solo accede a su propia carpeta de tickets + return send_from_directory(str(get_tickets_dir(session["usuario"])), filename) # ----------------------------------------------------------------------- # Gestion de usuarios (solo admin) @@ -341,9 +355,9 @@ def api_ocr_ticket(): fecha_ticket = date_match.group(1) if date_match else datetime.now().strftime('%d/%m/%Y') # Guardar como JSON en la carpeta tickets/ - ts = datetime.now().strftime('%Y%m%d_%H%M%S') - ticket_path = TICKETS_DIR / f"ocr_{ts}.json" - TICKETS_DIR.mkdir(exist_ok=True) + ts = datetime.now().strftime('%Y%m%d_%H%M%S') + tdir = get_tickets_dir(session['usuario']) + ticket_path = tdir / f"ocr_{ts}.json" with open(ticket_path, 'w', encoding='utf-8') as f: json.dump({ "fecha": fecha_ticket, diff --git a/autocompra7.py b/autocompra7.py index 95026a6..cc7a0a5 100644 --- a/autocompra7.py +++ b/autocompra7.py @@ -3,11 +3,20 @@ import pandas as pd import numpy as np import os import json +import argparse from datetime import datetime, timedelta from PyPDF2 import PdfReader -# Carpeta con los tickets PDF -ticket_folder = "tickets" +# --- Argumentos -------------------------------------------------------- +parser = argparse.ArgumentParser() +parser.add_argument('--usuario', default='default', help='Nombre del usuario') +args = parser.parse_args() + +# Carpeta con los tickets del usuario y directorio de salida +ticket_folder = os.path.join("tickets", args.usuario) +output_dir = os.path.join("datos", args.usuario) +os.makedirs(ticket_folder, exist_ok=True) +os.makedirs(output_dir, exist_ok=True) # Palabras clave que indican líneas que no hay que procesar exclude_keywords = [ @@ -87,7 +96,7 @@ df = pd.DataFrame(datos, columns=columnas) df.dropna(subset=["fecha"], inplace=True) # Guardar detalle completo -df.to_csv("detalle_productos.csv", index=False) +df.to_csv(os.path.join(output_dir, "detalle_productos.csv"), index=False) # Agrupar por producto resumen = df.groupby("producto").agg( @@ -97,12 +106,12 @@ resumen = df.groupby("producto").agg( primera_vez=("fecha", "min"), ultima_vez=("fecha", "max") ).sort_values("gasto_total", ascending=False) -resumen.to_csv("resumen_productos.csv") +resumen.to_csv(os.path.join(output_dir, "resumen_productos.csv")) # Gasto mensual df["mes"] = df["fecha"].dt.to_period("M") gasto_mensual = df.groupby("mes")["precio_total"].sum() -gasto_mensual.to_csv("gasto_mensual.csv") +gasto_mensual.to_csv(os.path.join(output_dir, "gasto_mensual.csv")) # --------------------------------------------------------------------------- # Cálculo de frecuencia con estacionalidad real por meses del año @@ -188,11 +197,11 @@ df = df.merge(resumen_estacional, on="producto", how="left") compra_estimacion = df[df["proxima_compra"] <= proxima_semana] # Guardar lista estimada -compra_estimacion.to_csv("compra_estimacion.csv", index=False) +compra_estimacion.to_csv(os.path.join(output_dir, "compra_estimacion.csv"), index=False) # Generar HTML visual html = compra_estimacion[["producto", "cantidad", "precio_unitario", "precio_total", "proxima_compra"]].sort_values("proxima_compra").to_html(index=False) -with open("lista_compra_estimada.html", "w") as file: +with open(os.path.join(output_dir, "lista_compra_estimada.html"), "w") as file: file.write(html) print("\n✅ Todo listo. Archivos generados:") @@ -207,5 +216,5 @@ lista_estimado = resumen_estacional.dropna(subset=["diferencia_dias"]).copy() lista_estimado["producto"] = lista_estimado["producto"].str.title() lista_estimado["fecha_estimada_proxima_compra"] = lista_estimado["proxima_compra"].dt.strftime("%d/%m/%Y") lista_estimado = lista_estimado.sort_values("diferencia_dias", ascending=True) -lista_estimado.to_csv("lista_compra_estimado.csv", index=False) +lista_estimado.to_csv(os.path.join(output_dir, "lista_compra_estimado.csv"), index=False) print("- lista_compra_estimado.csv") diff --git a/docker-compose.yml b/docker-compose.yml index 305beb4..0652947 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,11 +8,10 @@ SECRET_KEY: "${SECRET_KEY}" ADMIN_PASSWORD: "${ADMIN_PASSWORD}" volumes: - # Persistir tickets, datos generados y configuracion fuera del contenedor + # Persistir tickets y datos generados por usuario fuera del contenedor - ./tickets:/app/tickets - - ./datos.json:/app/datos.json + - ./datos:/app/datos - ./users.json:/app/users.json - ./config.ini:/app/config.ini - - ./lista_compra_estimado.csv:/app/lista_compra_estimado.csv ports: - "8088:5000" \ No newline at end of file diff --git a/generar_lista.py b/generar_lista.py index 6d28999..9ddfee3 100644 --- a/generar_lista.py +++ b/generar_lista.py @@ -6,8 +6,19 @@ Uso: python generar_lista.py import pandas as pd import json import re +import os +import argparse from datetime import datetime +# --- Argumentos -------------------------------------------------------- +parser = argparse.ArgumentParser() +parser.add_argument('--usuario', default='default', help='Nombre del usuario') +args = parser.parse_args() + +input_csv = os.path.join('datos', args.usuario, 'lista_compra_estimado.csv') +output_dir = os.path.join('datos', args.usuario) +os.makedirs(output_dir, exist_ok=True) + # --------------------------------------------------------------------------- # Filtros de basura (líneas del ticket que no son productos) # --------------------------------------------------------------------------- @@ -51,12 +62,12 @@ def limpiar_nombre(nombre): try: # Intentar UTF-8 primero, si falla usar cp1252 (Windows) try: - df = pd.read_csv('lista_compra_estimado.csv', encoding='utf-8') + df = pd.read_csv(input_csv, encoding='utf-8') except UnicodeDecodeError: - df = pd.read_csv('lista_compra_estimado.csv', encoding='cp1252') + df = pd.read_csv(input_csv, encoding='cp1252') except FileNotFoundError: - print("❌ No se encontró lista_compra_estimado.csv") - print(" Ejecuta primero: python autocompra5.py (o autocompra7.py)") + print("❌ No se encontró", input_csv) + print(" Ejecuta primero: python autocompra7.py --usuario", args.usuario) exit(1) print(f" Productos en CSV: {len(df)}") @@ -101,7 +112,7 @@ for _, row in df.iterrows(): # Escribir datos.js (cargable como