Multiples usuarios

This commit is contained in:
Tatiana Villa Ema 2026-04-25 13:32:50 +02:00
parent 5ecc8e7073
commit cea3be083f
4 changed files with 61 additions and 28 deletions

32
app.py
View File

@ -29,9 +29,17 @@ from werkzeug.security import generate_password_hash, check_password_hash
# Configuracion # Configuracion
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
BASE_DIR = Path(__file__).parent BASE_DIR = Path(__file__).parent
DATOS_JSON = BASE_DIR / "datos.json"
USERS_FILE = BASE_DIR / "users.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'} ALLOWED_IMAGE_TYPES = {'image/jpeg', 'image/jpg', 'image/png', 'image/webp'}
MAX_IMAGE_BYTES = 15 * 1024 * 1024 # 15 MB MAX_IMAGE_BYTES = 15 * 1024 * 1024 # 15 MB
@ -195,6 +203,9 @@ def registro():
"nombre": nombre "nombre": nombre
} }
guardar_usuarios(users) 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." ok = "Cuenta creada correctamente. Ya puedes iniciar sesion."
return render_template("registro.html", error=error, ok=ok) return render_template("registro.html", error=error, ok=ok)
@ -212,9 +223,10 @@ def index():
@app.route("/api/datos") @app.route("/api/datos")
@login_required @login_required
def api_datos(): 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 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) datos = json.load(f)
return jsonify(datos) return jsonify(datos)
@ -224,13 +236,14 @@ def api_datos():
@app.route("/api/regenerar", methods=["POST"]) @app.route("/api/regenerar", methods=["POST"])
@login_required @login_required
def api_regenerar(): def api_regenerar():
usuario = session["usuario"]
try: try:
subprocess.run( 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 cwd=str(BASE_DIR), check=True, capture_output=True, timeout=120
) )
subprocess.run( 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 cwd=str(BASE_DIR), check=True, capture_output=True, timeout=30
) )
return jsonify({"ok": True, "mensaje": "Pipeline ejecutado correctamente"}) return jsonify({"ok": True, "mensaje": "Pipeline ejecutado correctamente"})
@ -245,7 +258,8 @@ def api_regenerar():
@app.route("/tickets/<path:filename>") @app.route("/tickets/<path:filename>")
@login_required @login_required
def ticket_file(filename): 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) # Gestion de usuarios (solo admin)
@ -342,8 +356,8 @@ def api_ocr_ticket():
# Guardar como JSON en la carpeta tickets/ # Guardar como JSON en la carpeta tickets/
ts = datetime.now().strftime('%Y%m%d_%H%M%S') ts = datetime.now().strftime('%Y%m%d_%H%M%S')
ticket_path = TICKETS_DIR / f"ocr_{ts}.json" tdir = get_tickets_dir(session['usuario'])
TICKETS_DIR.mkdir(exist_ok=True) ticket_path = tdir / f"ocr_{ts}.json"
with open(ticket_path, 'w', encoding='utf-8') as f: with open(ticket_path, 'w', encoding='utf-8') as f:
json.dump({ json.dump({
"fecha": fecha_ticket, "fecha": fecha_ticket,

View File

@ -3,11 +3,20 @@ import pandas as pd
import numpy as np import numpy as np
import os import os
import json import json
import argparse
from datetime import datetime, timedelta from datetime import datetime, timedelta
from PyPDF2 import PdfReader from PyPDF2 import PdfReader
# Carpeta con los tickets PDF # --- Argumentos --------------------------------------------------------
ticket_folder = "tickets" 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 # Palabras clave que indican líneas que no hay que procesar
exclude_keywords = [ exclude_keywords = [
@ -87,7 +96,7 @@ df = pd.DataFrame(datos, columns=columnas)
df.dropna(subset=["fecha"], inplace=True) df.dropna(subset=["fecha"], inplace=True)
# Guardar detalle completo # 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 # Agrupar por producto
resumen = df.groupby("producto").agg( resumen = df.groupby("producto").agg(
@ -97,12 +106,12 @@ resumen = df.groupby("producto").agg(
primera_vez=("fecha", "min"), primera_vez=("fecha", "min"),
ultima_vez=("fecha", "max") ultima_vez=("fecha", "max")
).sort_values("gasto_total", ascending=False) ).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 # Gasto mensual
df["mes"] = df["fecha"].dt.to_period("M") df["mes"] = df["fecha"].dt.to_period("M")
gasto_mensual = df.groupby("mes")["precio_total"].sum() 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 # 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] compra_estimacion = df[df["proxima_compra"] <= proxima_semana]
# Guardar lista estimada # 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 # Generar HTML visual
html = compra_estimacion[["producto", "cantidad", "precio_unitario", "precio_total", "proxima_compra"]].sort_values("proxima_compra").to_html(index=False) 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) file.write(html)
print("\n✅ Todo listo. Archivos generados:") 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["producto"] = lista_estimado["producto"].str.title()
lista_estimado["fecha_estimada_proxima_compra"] = lista_estimado["proxima_compra"].dt.strftime("%d/%m/%Y") 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 = 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") print("- lista_compra_estimado.csv")

View File

@ -8,11 +8,10 @@
SECRET_KEY: "${SECRET_KEY}" SECRET_KEY: "${SECRET_KEY}"
ADMIN_PASSWORD: "${ADMIN_PASSWORD}" ADMIN_PASSWORD: "${ADMIN_PASSWORD}"
volumes: volumes:
# Persistir tickets, datos generados y configuracion fuera del contenedor # Persistir tickets y datos generados por usuario fuera del contenedor
- ./tickets:/app/tickets - ./tickets:/app/tickets
- ./datos.json:/app/datos.json - ./datos:/app/datos
- ./users.json:/app/users.json - ./users.json:/app/users.json
- ./config.ini:/app/config.ini - ./config.ini:/app/config.ini
- ./lista_compra_estimado.csv:/app/lista_compra_estimado.csv
ports: ports:
- "8088:5000" - "8088:5000"

View File

@ -6,8 +6,19 @@ Uso: python generar_lista.py
import pandas as pd import pandas as pd
import json import json
import re import re
import os
import argparse
from datetime import datetime 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) # Filtros de basura (líneas del ticket que no son productos)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -51,12 +62,12 @@ def limpiar_nombre(nombre):
try: try:
# Intentar UTF-8 primero, si falla usar cp1252 (Windows) # Intentar UTF-8 primero, si falla usar cp1252 (Windows)
try: try:
df = pd.read_csv('lista_compra_estimado.csv', encoding='utf-8') df = pd.read_csv(input_csv, encoding='utf-8')
except UnicodeDecodeError: except UnicodeDecodeError:
df = pd.read_csv('lista_compra_estimado.csv', encoding='cp1252') df = pd.read_csv(input_csv, encoding='cp1252')
except FileNotFoundError: except FileNotFoundError:
print("❌ No se encontró lista_compra_estimado.csv") print("❌ No se encontró", input_csv)
print(" Ejecuta primero: python autocompra5.py (o autocompra7.py)") print(" Ejecuta primero: python autocompra7.py --usuario", args.usuario)
exit(1) exit(1)
print(f" Productos en CSV: {len(df)}") print(f" Productos en CSV: {len(df)}")
@ -101,7 +112,7 @@ for _, row in df.iterrows():
# Escribir datos.js (cargable como <script src> sin servidor HTTP) # Escribir datos.js (cargable como <script src> sin servidor HTTP)
ts = datetime.now().strftime('%d/%m/%Y %H:%M') ts = datetime.now().strftime('%d/%m/%Y %H:%M')
with open('datos.js', 'w', encoding='utf-8') as f: with open(os.path.join(output_dir, 'datos.js'), 'w', encoding='utf-8') as f:
f.write('// Generado el ' + ts + ' - ' + str(len(resultado)) + ' productos\n') f.write('// Generado el ' + ts + ' - ' + str(len(resultado)) + ' productos\n')
f.write('const GENERADO = "' + ts + '";\n') f.write('const GENERADO = "' + ts + '";\n')
f.write('const predicciones = ') f.write('const predicciones = ')
@ -114,8 +125,8 @@ datos_json = {
'total': len(resultado), 'total': len(resultado),
'predicciones': resultado, 'predicciones': resultado,
} }
with open('datos.json', 'w', encoding='utf-8') as f: with open(os.path.join(output_dir, 'datos.json'), 'w', encoding='utf-8') as f:
json.dump(datos_json, f, ensure_ascii=False, indent=2) json.dump(datos_json, f, ensure_ascii=False, indent=2)
print(f"{len(resultado)} predicciones escritas en datos.js y datos.json") print(f"{len(resultado)} predicciones escritas en datos.json ({args.usuario})")
print(f" Abre index.html en el navegador o arranca app.py para ver la lista.") print(f" Abre index.html en el navegador o arranca app.py para ver la lista.")