Actualizar backend/services.py

This commit is contained in:
2026-06-28 23:03:21 -05:00
parent 725cc6f818
commit f886370d5c

View File

@@ -1,4 +1,3 @@
# backend/services.py
""" """
Capa de servicios: envuelve la lógica existente (modules/) con caché. Capa de servicios: envuelve la lógica existente (modules/) con caché.
NO modifica la lógica de negocio — solo la llama y serializa el resultado. NO modifica la lógica de negocio — solo la llama y serializa el resultado.
@@ -14,14 +13,12 @@ from cache_manager import cache_get_or_set
# Instancia única del DataManager (como @st.cache_resource) # Instancia única del DataManager (como @st.cache_resource)
_DM = None _DM = None
def get_dm() -> DataManager: def get_dm() -> DataManager:
global _DM global _DM
if _DM is None: if _DM is None:
_DM = DataManager() _DM = DataManager()
return _DM return _DM
def _serializar_dicts(datos): def _serializar_dicts(datos):
out = [] out = []
for d in (datos or []): for d in (datos or []):
@@ -31,7 +28,6 @@ def _serializar_dicts(datos):
out.append(fila) out.append(fila)
return out return out
def _serializar_filas(filas, limite=None): def _serializar_filas(filas, limite=None):
out = [] out = []
for f in (filas or []): for f in (filas or []):
@@ -39,21 +35,30 @@ def _serializar_filas(filas, limite=None):
out.append([str(v) if v is not None else "" for v in vals]) out.append([str(v) if v is not None else "" for v in vals])
return out return out
# ── OCUPABILIDAD ─────────────────────────────────────────────────────────── # ── OCUPABILIDAD ───────────────────────────────────────────────────────────
def ocupabilidad(ano, mes, sede="TODOS", programa="TODOS"): def ocupabilidad(ano, mes, sede="TODOS", programa="TODOS"):
def _load(): def _load():
dm = get_dm() dm = get_dm()
analizador = AnalizadorCursos(dm) analizador = AnalizadorCursos(dm)
print(f"[DEBUG Ocupabilidad] Buscando ano={ano}, mes={mes}, sede={sede}, programa={programa}")
# Intento 1: Formato original (ej: "6")
datos = analizador.obtener_datos_procesados(str(ano), str(mes), sede, programa) datos = analizador.obtener_datos_procesados(str(ano), str(mes), sede, programa)
# Intento 2: Si viene vacío, probar con formato de dos dígitos (ej: "06")
if not datos and len(str(mes)) == 1:
mes_padded = str(mes).zfill(2)
print(f"[DEBUG Ocupabilidad] Sin resultados. Reintentando con mes acolchado: '{mes_padded}'")
datos = analizador.obtener_datos_procesados(str(ano), mes_padded, sede, programa)
filas = analizador.formatear_datos_para_tabla(datos) filas = analizador.formatear_datos_para_tabla(datos)
print(f"[DEBUG Ocupabilidad] Proceso completado. Registros encontrados: {len(datos) if datos else 0}")
return { return {
"datos": _serializar_dicts(datos), "datos": _serializar_dicts(datos),
"filas": _serializar_filas(filas), "filas": _serializar_filas(filas),
} }
return cache_get_or_set("ocupabilidad", (ano, mes, sede, programa), _load) return cache_get_or_set("ocupabilidad", (ano, mes, sede, programa), _load)
# ── VENTAS ───────────────────────────────────────────────────────────────── # ── VENTAS ─────────────────────────────────────────────────────────────────
def ventas(ano, mes, sede="TODOS", programa="TODOS"): def ventas(ano, mes, sede="TODOS", programa="TODOS"):
def _load(): def _load():
@@ -62,17 +67,28 @@ def ventas(ano, mes, sede="TODOS", programa="TODOS"):
dm._fecha_canc_overrides = comisiones_overrides_fecha() # {mat: fecha_canc override} dm._fecha_canc_overrides = comisiones_overrides_fecha() # {mat: fecha_canc override}
dm._inv_neta_overrides = comisiones_overrides_inv_neta() # {mat: inversion_neta override} dm._inv_neta_overrides = comisiones_overrides_inv_neta() # {mat: inversion_neta override}
logic = VentasLogic(dm) logic = VentasLogic(dm)
datos = logic.obtener_datos_brutos_filtrado(str(ano), str(mes), sede, programa) \ print(f"[DEBUG Ventas] Buscando ano={ano}, mes={mes}, sede={sede}, programa={programa}")
if hasattr(logic, "obtener_datos_brutos_filtrado") else logic.obtener_datos_brutos(str(ano), str(mes))
datos = [d for d in datos if d.get('VENDEDOR', 'SIN VENDEDOR') != 'SIN VENDEDOR'] mes_str = str(mes)
datos = logic.obtener_datos_brutos_filtrado(str(ano), mes_str, sede, programa) \
if hasattr(logic, "obtener_datos_brutos_filtrado") else logic.obtener_datos_brutos(str(ano), mes_str)
# Intento 2: Mes de dos dígitos si el original no trajo nada
if not datos and len(mes_str) == 1:
mes_padded = mes_str.zfill(2)
print(f"[DEBUG Ventas] Sin resultados. Reintentando con mes acolchado: '{mes_padded}'")
datos = logic.obtener_datos_brutos_filtrado(str(ano), mes_padded, sede, programa) \
if hasattr(logic, "obtener_datos_brutos_filtrado") else logic.obtener_datos_brutos(str(ano), mes_padded)
datos = [d for d in (datos or []) if d.get('VENDEDOR', 'SIN VENDEDOR') != 'SIN VENDEDOR']
filas = logic.formatear_datos_para_tabla(datos) filas = logic.formatear_datos_para_tabla(datos)
print(f"[DEBUG Ventas] Proceso completado. Registros encontrados: {len(datos)}")
return { return {
"filas": _serializar_filas(filas), "filas": _serializar_filas(filas),
"datos": _serializar_dicts(datos), "datos": _serializar_dicts(datos),
} }
return cache_get_or_set("ventas", (ano, mes, sede, programa), _load) return cache_get_or_set("ventas", (ano, mes, sede, programa), _load)
def _clasif_filtro_programa(dm, dscp): def _clasif_filtro_programa(dm, dscp):
"""Misma clasificación de programa que ejecutar_consulta_ventas (SEMINARIOS/TEAC/TERC/OTROS).""" """Misma clasificación de programa que ejecutar_consulta_ventas (SEMINARIOS/TEAC/TERC/OTROS)."""
up = str(dscp or "").upper() up = str(dscp or "").upper()
@@ -83,7 +99,6 @@ def _clasif_filtro_programa(dm, dscp):
return cat return cat
return fdata.get("DEFAULT", "OTROS") if isinstance(fdata.get("DEFAULT"), str) else "OTROS" return fdata.get("DEFAULT", "OTROS") if isinstance(fdata.get("DEFAULT"), str) else "OTROS"
def ventas_detalle(vendedor, ano, mes, tipo_lista, sede="TODOS", programa="TODOS"): def ventas_detalle(vendedor, ano, mes, tipo_lista, sede="TODOS", programa="TODOS"):
def _load(): def _load():
dm = get_dm() dm = get_dm()
@@ -93,8 +108,7 @@ def ventas_detalle(vendedor, ano, mes, tipo_lista, sede="TODOS", programa="TODOS
resultado = logic.obtener_detalle_vendedor(vendedor, str(ano), str(mes)) resultado = logic.obtener_detalle_vendedor(vendedor, str(ano), str(mes))
filas = resultado.get(tipo_lista, []) if isinstance(resultado, dict) else [] filas = resultado.get(tipo_lista, []) if isinstance(resultado, dict) else []
filas = _aplicar_overrides_comisiones(filas) filas = _aplicar_overrides_comisiones(filas)
# Filtro por SEDE [14] y PROGRAMA (clasificado desde el nombre [16]),
# mismas reglas que la tabla principal. Omite la fila TOTAL GENERAL.
_sede = str(sede or "TODOS").upper() _sede = str(sede or "TODOS").upper()
_prog = str(programa or "TODOS").upper() _prog = str(programa or "TODOS").upper()
if _sede != "TODOS" or _prog != "TODOS": if _sede != "TODOS" or _prog != "TODOS":
@@ -106,8 +120,6 @@ def ventas_detalle(vendedor, ano, mes, tipo_lista, sede="TODOS", programa="TODOS
out.append(f); continue out.append(f); continue
if _sede != "TODOS" and str(f[14]).strip().upper() != _sede: if _sede != "TODOS" and str(f[14]).strip().upper() != _sede:
continue continue
# Clasificar programa por el nombre de CLASIFICACIÓN [16] (prioriza dsc_programa);
# si no existe, usar el mostrado [8].
_prog_nombre = f[16] if len(f) > 16 else f[8] _prog_nombre = f[16] if len(f) > 16 else f[8]
if _prog != "TODOS" and _clasif_filtro_programa(dm, _prog_nombre) != _prog: if _prog != "TODOS" and _clasif_filtro_programa(dm, _prog_nombre) != _prog:
continue continue
@@ -116,10 +128,7 @@ def ventas_detalle(vendedor, ano, mes, tipo_lista, sede="TODOS", programa="TODOS
return {"filas": _serializar_filas(filas)} return {"filas": _serializar_filas(filas)}
return cache_get_or_set("ventas_det", (vendedor, ano, mes, tipo_lista, sede, programa), _load) return cache_get_or_set("ventas_det", (vendedor, ano, mes, tipo_lista, sede, programa), _load)
def comisiones_detalle_todos(ano, mes): def comisiones_detalle_todos(ano, mes):
"""Devuelve TODOS los matriculados (todos los años/meses) para el buscador.
Aplica overrides de Supabase. Cacheado globalmente (no depende del mes)."""
def _load(): def _load():
dm = get_dm() dm = get_dm()
logic = VentasLogic(dm) logic = VentasLogic(dm)
@@ -128,9 +137,7 @@ def comisiones_detalle_todos(ano, mes):
return {"filas": _serializar_filas(filas)} return {"filas": _serializar_filas(filas)}
return cache_get_or_set("comisiones_det_todos", ("GLOBAL",), _load) return cache_get_or_set("comisiones_det_todos", ("GLOBAL",), _load)
def comisiones_overrides_fecha(): def comisiones_overrides_fecha():
"""Devuelve dict {num_matricula: fecha_cancelacion1_override} para clasificación."""
try: try:
sb = _sb() sb = _sb()
res = sb.table("comisiones_overrides").select("num_matricula,fecha_cancelacion1").execute() res = sb.table("comisiones_overrides").select("num_matricula,fecha_cancelacion1").execute()
@@ -139,8 +146,6 @@ def comisiones_overrides_fecha():
mat = str(r["num_matricula"]) mat = str(r["num_matricula"])
v = r.get("fecha_cancelacion1") v = r.get("fecha_cancelacion1")
sv = "" if v is None else str(v).strip() sv = "" if v is None else str(v).strip()
# Solo "__VACIO__" = fecha borrada a propósito → anula la del SQL.
# None/"" = la fila tiene override de OTROS campos pero no toco la fecha -> usar SQL.
if sv == "__VACIO__": if sv == "__VACIO__":
out[mat] = "__VACIO__" out[mat] = "__VACIO__"
elif sv != "": elif sv != "":
@@ -149,10 +154,7 @@ def comisiones_overrides_fecha():
except Exception: except Exception:
return {} return {}
def comisiones_overrides_inv_neta(): def comisiones_overrides_inv_neta():
"""Devuelve dict {num_matricula: inversion_neta(float)} para la tabla principal.
El valor es el monto final en soles (no se re-convierte por TC)."""
try: try:
sb = _sb() sb = _sb()
res = sb.table("comisiones_overrides").select("num_matricula,inversion_neta").execute() res = sb.table("comisiones_overrides").select("num_matricula,inversion_neta").execute()
@@ -164,19 +166,14 @@ def comisiones_overrides_inv_neta():
s = str(v).replace("S/", "").replace(",", "").strip() s = str(v).replace("S/", "").replace(",", "").strip()
if s == "": if s == "":
continue continue
try: try: out[str(r["num_matricula"])] = float(s)
out[str(r["num_matricula"])] = float(s)
except Exception: except Exception:
pass pass
return out return out
except Exception: except Exception:
return {} return {}
def _aplicar_overrides_comisiones(filas): def _aplicar_overrides_comisiones(filas):
"""Sobreescribe valores de cada fila con los guardados en Supabase (por num_matricula).
Índices de fila: [2]F.MAT [7]INV.NETA [9]F.INI [10]SALDO MAT [11]SALDO C1
[12]DÍAS [13]VALOR ADIC [14]TIPO PROG [15]num_matricula. (5=PROMEDIO CUOTA)"""
try: try:
dm = get_dm() dm = get_dm()
if not getattr(dm, "supabase_client", None): if not getattr(dm, "supabase_client", None):
@@ -185,7 +182,6 @@ def _aplicar_overrides_comisiones(filas):
ov = {str(r["num_matricula"]): r for r in (res.data or [])} ov = {str(r["num_matricula"]): r for r in (res.data or [])}
if not ov: if not ov:
return filas return filas
# columna override -> índice en la fila
mapa = { mapa = {
"fch_matricula": 2, "promedio_cuota": 5, "inversion_neta": 7, "fch_matricula": 2, "promedio_cuota": 5, "inversion_neta": 7,
"fecha_inicio": 9, "saldo_matricula": 10, "saldo_cuota1": 11, "fecha_inicio": 9, "saldo_matricula": 10, "saldo_cuota1": 11,
@@ -196,10 +192,8 @@ def _aplicar_overrides_comisiones(filas):
if len(f) < 16: if len(f) < 16:
continue continue
mat = str(f[15]) mat = str(f[15])
# [16] = nombre para clasificar (si no vino, usar el mostrado [8])
# [17] = flag "tiene override" (0 por defecto)
if len(f) == 16: if len(f) == 16:
f.append(f[8]) # buscador: no trae [16] → reutiliza el nombre mostrado f.append(f[8])
if len(f) == 17: if len(f) == 17:
f.append("0") f.append("0")
if mat in ov: if mat in ov:
@@ -209,30 +203,38 @@ def _aplicar_overrides_comisiones(filas):
if val is None: if val is None:
continue continue
if str(val) == "__VACIO__": if str(val) == "__VACIO__":
f[idx] = "" # forzar vacío explícito (override "borrar") f[idx] = ""
elif str(val) != "": elif str(val) != "":
f[idx] = val f[idx] = val
f[17] = "1" # marcar fila como editada f[17] = "1"
return filas return filas
except Exception as e: except Exception as e:
print(f"[overrides comisiones] {e}") print(f"[overrides comisiones] {e}")
return filas return filas
# ── COBRANZA ─────────────────────────────────────────────────────────────── # ── COBRANZA ───────────────────────────────────────────────────────────────
def cobranza(ano, mes, sectorista="TODOS", agrupacion="SEDE"): def cobranza(ano, mes, sectorista="TODOS", agrupacion="SEDE"):
def _load(): def _load():
dm = get_dm() dm = get_dm()
logic = CobranzaLogic(dm) logic = CobranzaLogic(dm)
filas = logic.obtener_datos_tabla(str(ano), str(mes), sectorista, agrupacion) print(f"[DEBUG Cobranza] Buscando ano={ano}, mes={mes}, sectorista={sectorista}, agrupacion={agrupacion}")
sectoristas = logic.obtener_lista_sectoristas(str(ano), str(mes))
mes_str = str(mes)
filas = logic.obtener_datos_tabla(str(ano), mes_str, sectorista, agrupacion)
if not filas and len(mes_str) == 1:
mes_padded = mes_str.zfill(2)
print(f"[DEBUG Cobranza] Sin resultados. Reintentando con mes acolchado: '{mes_padded}'")
filas = logic.obtener_datos_tabla(str(ano), mes_padded, sectorista, agrupacion)
sectoristas = logic.obtener_lista_sectoristas(str(ano), mes_str)
print(f"[DEBUG Cobranza] Proceso completado. Filas encontradas: {len(filas) if filas else 0}")
return { return {
"filas": _serializar_filas(filas), "filas": _serializar_filas(filas),
"sectoristas": sectoristas or ["TODOS"], "sectoristas": sectoristas or ["TODOS"],
} }
return cache_get_or_set("cobranza", (ano, mes, sectorista, agrupacion), _load) return cache_get_or_set("cobranza", (ano, mes, sectorista, agrupacion), _load)
def cobranza_detalle(grupo, ano, mes, sectorista, agrupacion="PROGRAMA"): def cobranza_detalle(grupo, ano, mes, sectorista, agrupacion="PROGRAMA"):
def _load(): def _load():
dm = get_dm() dm = get_dm()
@@ -240,7 +242,6 @@ def cobranza_detalle(grupo, ano, mes, sectorista, agrupacion="PROGRAMA"):
if agrupacion == "ASESOR": if agrupacion == "ASESOR":
datos = logic.obtener_detalle_programa_formateado(str(ano), str(mes), grupo, "TODOS") datos = logic.obtener_detalle_programa_formateado(str(ano), str(mes), grupo, "TODOS")
elif agrupacion == "SEDE": elif agrupacion == "SEDE":
# Filtrar por sede usando el MISMO clasificador del processor (sede.json)
crudos = logic.processor.obtener_detalle_programa(str(ano), str(mes), sectorista, "TODOS") crudos = logic.processor.obtener_detalle_programa(str(ano), str(mes), sectorista, "TODOS")
grupo_up = str(grupo).upper() grupo_up = str(grupo).upper()
mats_sede = set() mats_sede = set()
@@ -250,14 +251,12 @@ def cobranza_detalle(grupo, ano, mes, sectorista, agrupacion="PROGRAMA"):
if sede_det == grupo_up: if sede_det == grupo_up:
mats_sede.add(str(d.get("MATRICULA", ""))) mats_sede.add(str(d.get("MATRICULA", "")))
todas = logic.obtener_detalle_programa_formateado(str(ano), str(mes), sectorista, "TODOS") todas = logic.obtener_detalle_programa_formateado(str(ano), str(mes), sectorista, "TODOS")
# Solo alumnos de la sede (sin la fila TOTAL global; el frontend recalcula el total)
datos = [f for f in (todas or []) if str(f[0]) in mats_sede] datos = [f for f in (todas or []) if str(f[0]) in mats_sede]
else: # PROGRAMA else:
datos = logic.obtener_detalle_programa_formateado(str(ano), str(mes), sectorista, grupo) datos = logic.obtener_detalle_programa_formateado(str(ano), str(mes), sectorista, grupo)
return {"filas": _serializar_filas(datos)} return {"filas": _serializar_filas(datos)}
return cache_get_or_set("cobranza_det", (grupo, ano, mes, sectorista, agrupacion), _load) return cache_get_or_set("cobranza_det", (grupo, ano, mes, sectorista, agrupacion), _load)
# ── RENTABILIDAD ─────────────────────────────────────────────────────────── # ── RENTABILIDAD ───────────────────────────────────────────────────────────
def rentabilidad(ano, mes, sede="TODOS", filtro_prog="TODOS"): def rentabilidad(ano, mes, sede="TODOS", filtro_prog="TODOS"):
def _load(): def _load():
@@ -270,7 +269,6 @@ def rentabilidad(ano, mes, sede="TODOS", filtro_prog="TODOS"):
return {"filas": filas_s, "programas": nombres} return {"filas": filas_s, "programas": nombres}
return cache_get_or_set("rentabilidad", (ano, mes, sede, filtro_prog), _load) return cache_get_or_set("rentabilidad", (ano, mes, sede, filtro_prog), _load)
def rentabilidad_detalle(programa, ano, mes): def rentabilidad_detalle(programa, ano, mes):
def _load(): def _load():
dm = get_dm() dm = get_dm()
@@ -279,7 +277,6 @@ def rentabilidad_detalle(programa, ano, mes):
return {"filas": _serializar_filas(resultado)} return {"filas": _serializar_filas(resultado)}
return cache_get_or_set("rentabilidad_det", (programa, ano, mes), _load) return cache_get_or_set("rentabilidad_det", (programa, ano, mes), _load)
def rentabilidad_costos(programa, ano, mes): def rentabilidad_costos(programa, ano, mes):
def _load(): def _load():
dm = get_dm() dm = get_dm()
@@ -287,7 +284,6 @@ def rentabilidad_costos(programa, ano, mes):
return {"costos": logic.obtener_datos_costos_programa(programa, str(ano), str(mes))} return {"costos": logic.obtener_datos_costos_programa(programa, str(ano), str(mes))}
return cache_get_or_set("rentabilidad_costos", (programa, ano, mes), _load) return cache_get_or_set("rentabilidad_costos", (programa, ano, mes), _load)
# ── SALDO PENDIENTE ──────────────────────────────────────────────────────── # ── SALDO PENDIENTE ────────────────────────────────────────────────────────
def saldo_pendiente(tipo_cuota): def saldo_pendiente(tipo_cuota):
def _load(): def _load():
@@ -297,8 +293,7 @@ def saldo_pendiente(tipo_cuota):
return {"datos": _serializar_dicts(datos)} return {"datos": _serializar_dicts(datos)}
return cache_get_or_set("saldo", (tipo_cuota,), _load) return cache_get_or_set("saldo", (tipo_cuota,), _load)
# ── PRECARGA GLOBAL ────────────────────────────────────────────────────────
# ── PRECARGA GLOBAL (calienta todo el caché) ───────────────────────────────
def precargar_todo(): def precargar_todo():
from datetime import datetime from datetime import datetime
ahora = datetime.now() ahora = datetime.now()
@@ -318,16 +313,13 @@ def precargar_todo():
except Exception as e: except Exception as e:
print(f"[precarga] error: {e}") print(f"[precarga] error: {e}")
_refresh_count = 0 _refresh_count = 0
def _recalcular_clave(key): def _recalcular_clave(key):
"""Dada una clave 'prefijo:arg1|arg2|...', recalcula su valor sin usar el caché viejo."""
from cache_manager import cache_invalidate from cache_manager import cache_invalidate
try: try:
prefijo, _, resto = key.partition(":") prefijo, _, resto = key.partition(":")
args = resto.split("|") if resto else [] args = resto.split("|") if resto else []
# Invalidar SOLO esta entrada para forzar su recálculo
cache_invalidate(prefijo + ":" + resto if resto else prefijo) cache_invalidate(prefijo + ":" + resto if resto else prefijo)
if prefijo == "ocupabilidad" and len(args) == 4: if prefijo == "ocupabilidad" and len(args) == 4:
return ocupabilidad(args[0], args[1], args[2], args[3]) return ocupabilidad(args[0], args[1], args[2], args[3])
@@ -343,31 +335,24 @@ def _recalcular_clave(key):
return cobranza_detalle_todos(args[0], args[1], args[2]) return cobranza_detalle_todos(args[0], args[1], args[2])
except Exception as e: except Exception as e:
print(f"[recalcular] {key}: {e}") print(f"[recalcular] {key}: {e}")
return None # los detalles puntuales se recalculan al pedirse return None
def refrescar_todo(): def refrescar_todo():
"""Refresca SOLO las entradas ya cacheadas (no vacía el caché), para que los
meses ya visitados se mantengan rápidos. Cada ~1h recarga config de GitHub."""
global _refresh_count global _refresh_count
from cache_manager import cache_refresh_existing, cache_keys from cache_manager import cache_refresh_existing, cache_keys
_refresh_count += 1 _refresh_count += 1
# El hilo corre cada 900s (15 min); 4 ciclos ≈ 60 min → recargar config GitHub
if _refresh_count % 4 == 0: if _refresh_count % 4 == 0:
try: try:
get_dm().cargar_toda_configuracion() get_dm().cargar_toda_configuracion()
print("[config] Recarga de queries/JSON de GitHub completada") print("[config] Recarga de queries/JSON de GitHub completada")
except Exception as e: except Exception as e:
print(f"[config] error al recargar GitHub: {e}") print(f"[config] error al recargar GitHub: {e}")
# Si el caché está vacío (primer arranque), hacer precarga normal
if not cache_keys(): if not cache_keys():
precargar_todo() precargar_todo()
else: else:
cache_refresh_existing(_recalcular_clave) cache_refresh_existing(_recalcular_clave)
# ── OPERACIONES EXTRA ──────────────────────────────────────────────────────
# ── GUARDAR COSTOS (override Supabase) ─────────────────────────────────────
def guardar_costos(num_indice, costos_inicial, costos_actual): def guardar_costos(num_indice, costos_inicial, costos_actual):
dm = get_dm() dm = get_dm()
ok1 = dm.guardar_override_costo(num_indice, "inicial", costos_inicial) ok1 = dm.guardar_override_costo(num_indice, "inicial", costos_inicial)
@@ -377,17 +362,12 @@ def guardar_costos(num_indice, costos_inicial, costos_actual):
cache_invalidate("rentabilidad_costos") cache_invalidate("rentabilidad_costos")
return bool(ok1 and ok2) return bool(ok1 and ok2)
# ── CLASIFICACIÓN DE SEDE (sede.json) ──────────────────────────────────────
def clasificar_programas(nombres): def clasificar_programas(nombres):
"""Devuelve {programa: sede} usando el clasificador del processor (sede.json)."""
dm = get_dm() dm = get_dm()
logic = CobranzaLogic(dm) logic = CobranzaLogic(dm)
return {n: str(logic.processor.clasificar_sede(n)) for n in nombres} return {n: str(logic.processor.clasificar_sede(n)) for n in nombres}
def cobranza_detalle_todos(ano, mes, sectorista="TODOS"): def cobranza_detalle_todos(ano, mes, sectorista="TODOS"):
"""Trae TODOS los alumnos en UNA sola consulta (para export y buscador)."""
def _load(): def _load():
dm = get_dm() dm = get_dm()
logic = CobranzaLogic(dm) logic = CobranzaLogic(dm)
@@ -415,8 +395,7 @@ def cobranza_detalle_todos(ano, mes, sectorista="TODOS"):
return {"alumnos": alumnos} return {"alumnos": alumnos}
return cache_get_or_set("cobranza_det_todos", (ano, mes, sectorista), _load) return cache_get_or_set("cobranza_det_todos", (ano, mes, sectorista), _load)
# ── GESTIÓN DE USUARIOS ────────────────────────────────────────────────────
# ── GESTIÓN DE USUARIOS (Supabase Auth + tabla perfiles) ───────────────────
def _sb(): def _sb():
dm = get_dm() dm = get_dm()
if not dm.supabase_client: if not dm.supabase_client:
@@ -465,17 +444,13 @@ def usuarios_eliminar(user_id):
sb.auth.admin.delete_user(user_id) sb.auth.admin.delete_user(user_id)
return {"status": "ok"} return {"status": "ok"}
# ── CONFIG MENSUAL COMISIONES ──────────────────────────────────────────────
# ── VENDEDORES MANUALES (Comisiones, en Supabase) ──────────────────────────
# ── CONFIG MENSUAL COMISIONES (IMP_TC, META, COMISIÓN por tipo de programa) ──
def comisiones_config_listar(ano, mes): def comisiones_config_listar(ano, mes):
"""Lista la config de un mes (todas las filas por tipo_programa)."""
sb = _sb() sb = _sb()
res = sb.table("comisiones_config_mensual").select("*").eq("ano", int(ano)).eq("mes", int(mes)).execute() res = sb.table("comisiones_config_mensual").select("*").eq("ano", int(ano)).eq("mes", int(mes)).execute()
return {"config": res.data or []} return {"config": res.data or []}
def comisiones_config_guardar(ano, mes, filas): def comisiones_config_guardar(ano, mes, filas):
"""Reemplaza la config del mes. filas = [{tipo_programa, imp_tc, meta, comision}]."""
sb = _sb() sb = _sb()
sb.table("comisiones_config_mensual").delete().eq("ano", int(ano)).eq("mes", int(mes)).execute() sb.table("comisiones_config_mensual").delete().eq("ano", int(ano)).eq("mes", int(mes)).execute()
payload = [] payload = []
@@ -495,7 +470,6 @@ def comisiones_config_guardar(ano, mes, filas):
return {"status": "ok"} return {"status": "ok"}
def comisiones_tc_del_mes(ano, mes): def comisiones_tc_del_mes(ano, mes):
"""Devuelve el IMP_TC configurado para el mes (toma el primero que tenga TC > 0), o None."""
try: try:
sb = _sb() sb = _sb()
res = sb.table("comisiones_config_mensual").select("imp_tc").eq("ano", int(ano)).eq("mes", int(mes)).execute() res = sb.table("comisiones_config_mensual").select("imp_tc").eq("ano", int(ano)).eq("mes", int(mes)).execute()
@@ -506,11 +480,10 @@ def comisiones_tc_del_mes(ano, mes):
pass pass
return None return None
def vendedores_manuales_listar(ano, mes): def vendedores_manuales_listar(ano, mes):
sb = _sb() sb = _sb()
query = sb.table("vendedores_manuales").select("*").eq("ano", int(ano)) query = sb.table("vendedores_manuales").select("*").eq("ano", int(ano))
if int(mes) != 0: # mes=0 → traer todo el año (para cache en frontend) if int(mes) != 0:
query = query.eq("mes", int(mes)) query = query.eq("mes", int(mes))
res = query.execute() res = query.execute()
return {"vendedores": res.data or []} return {"vendedores": res.data or []}
@@ -530,7 +503,6 @@ def vendedores_manuales_eliminar(id):
sb.table("vendedores_manuales").delete().eq("id", id).execute() sb.table("vendedores_manuales").delete().eq("id", id).execute()
return {"status": "ok"} return {"status": "ok"}
def vendedores_manuales_actualizar(id, descripcion, fch_emision, monto): def vendedores_manuales_actualizar(id, descripcion, fch_emision, monto):
sb = _sb() sb = _sb()
sb.table("vendedores_manuales").update({ sb.table("vendedores_manuales").update({
@@ -539,14 +511,9 @@ def vendedores_manuales_actualizar(id, descripcion, fch_emision, monto):
}).eq("id", id).execute() }).eq("id", id).execute()
return {"status": "ok"} return {"status": "ok"}
def vendedores_manuales_guardar_lote(nombre, ano, mes, filas): def vendedores_manuales_guardar_lote(nombre, ano, mes, filas):
"""Reemplaza TODAS las filas de un vendedor manual en una sola operación.
filas = lista de dicts {descripcion, fch_emision, monto}. Si filas vacía → elimina el vendedor."""
sb = _sb() sb = _sb()
# Borrar las existentes de ese vendedor/mes
sb.table("vendedores_manuales").delete().eq("nombre", nombre).eq("ano", int(ano)).eq("mes", int(mes)).execute() sb.table("vendedores_manuales").delete().eq("nombre", nombre).eq("ano", int(ano)).eq("mes", int(mes)).execute()
# Insertar las nuevas en un solo batch
payload = [] payload = []
for f in filas: for f in filas:
payload.append({ payload.append({
@@ -560,12 +527,8 @@ def vendedores_manuales_guardar_lote(nombre, ano, mes, filas):
sb.table("vendedores_manuales").insert(payload).execute() sb.table("vendedores_manuales").insert(payload).execute()
return {"status": "ok"} return {"status": "ok"}
# ── OVERRIDES DE COMISIONES ────────────────────────────────────────────────
# ── OVERRIDES DE COMISIONES (editar valores por num_matricula) ─────────────
def comisiones_override_guardar(registros): def comisiones_override_guardar(registros):
"""Guarda/actualiza overrides en UNA sola operación (batch upsert)."""
sb = _sb() sb = _sb()
from cache_manager import cache_invalidate from cache_manager import cache_invalidate
validos = [r for r in registros if r.get("num_matricula")] validos = [r for r in registros if r.get("num_matricula")]
@@ -577,11 +540,10 @@ def comisiones_override_guardar(registros):
return {"status": "ok"} return {"status": "ok"}
def comisiones_override_restaurar(num_matricula): def comisiones_override_restaurar(num_matricula):
"""Borra el override de un alumno (vuelve a los valores de SQL)."""
sb = _sb() sb = _sb()
from cache_manager import cache_invalidate from cache_manager import cache_invalidate
sb.table("comisiones_overrides").delete().eq("num_matricula", str(num_matricula)).execute() sb.table("comisiones_overrides").delete().eq("num_matricula", str(num_matricula)).execute()
cache_invalidate("ventas_det") cache_invalidate("ventas_det")
cache_invalidate("ventas") cache_invalidate("ventas")
cache_invalidate("comisiones_det_todos") cache_invalidate("comisiones_det_todos")
return {"status": "ok"} return {"status": "ok"}