Actualizar backend/services.py
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
# backend/services.py
|
||||
"""
|
||||
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.
|
||||
@@ -25,12 +26,14 @@ def ultima_actualizacion():
|
||||
"""Devuelve la hora (ISO) del último refresco de datos, o None."""
|
||||
return {"hora": _ultima_actualizacion.isoformat() if _ultima_actualizacion else None}
|
||||
|
||||
|
||||
def get_dm() -> DataManager:
|
||||
global _DM
|
||||
if _DM is None:
|
||||
_DM = DataManager()
|
||||
return _DM
|
||||
|
||||
|
||||
def _serializar_dicts(datos):
|
||||
out = []
|
||||
for d in (datos or []):
|
||||
@@ -40,6 +43,7 @@ def _serializar_dicts(datos):
|
||||
out.append(fila)
|
||||
return out
|
||||
|
||||
|
||||
def _serializar_filas(filas, limite=None):
|
||||
out = []
|
||||
for f in (filas or []):
|
||||
@@ -47,24 +51,14 @@ def _serializar_filas(filas, limite=None):
|
||||
out.append([str(v) if v is not None else "" for v in vals])
|
||||
return out
|
||||
|
||||
|
||||
# ── OCUPABILIDAD ───────────────────────────────────────────────────────────
|
||||
def ocupabilidad(ano, mes, sede="TODOS", programa="TODOS"):
|
||||
def _load():
|
||||
dm = get_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)
|
||||
|
||||
# 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)
|
||||
print(f"[DEBUG Ocupabilidad] Proceso completado. Registros encontrados: {len(datos) if datos else 0}")
|
||||
return {
|
||||
"datos": _serializar_dicts(datos),
|
||||
"filas": _serializar_filas(filas),
|
||||
@@ -89,39 +83,29 @@ def ventas(ano, mes, sede="TODOS", programa="TODOS", con_overrides=False):
|
||||
# Los overrides de Supabase SOLO aplican en Comisiones (con_overrides=True).
|
||||
# En Ventas se ignoran: la tabla muestra los datos reales del SQL.
|
||||
if con_overrides:
|
||||
dm._tc_override_mes = comisiones_tc_del_mes(ano, mes)
|
||||
dm._fecha_canc_overrides = comisiones_overrides_fecha()
|
||||
dm._inv_neta_overrides = comisiones_overrides_inv_neta()
|
||||
dm._tc_override_mes = comisiones_tc_del_mes(ano, mes) # TC de Supabase o None
|
||||
dm._fecha_canc_overrides = comisiones_overrides_fecha() # {mat: fecha_canc override}
|
||||
dm._inv_neta_overrides = comisiones_overrides_inv_neta() # {mat: inv_neta override}
|
||||
else:
|
||||
dm._tc_override_mes = None
|
||||
dm._fecha_canc_overrides = {}
|
||||
dm._inv_neta_overrides = {}
|
||||
logic = VentasLogic(dm)
|
||||
print(f"[DEBUG Ventas] Buscando ano={ano}, mes={mes}, sede={sede}, programa={programa}")
|
||||
|
||||
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']
|
||||
datos = logic.obtener_datos_brutos_filtrado(str(ano), str(mes), sede, 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']
|
||||
|
||||
# PENDIENTES: recalcular desde el MISMO detalle del "Ver" (__TODOS__), para que
|
||||
# la tabla principal y el popup Ver salgan SIEMPRE idénticos (misma fuente/regla).
|
||||
try:
|
||||
det = logic.obtener_detalle_vendedor("__TODOS__", str(ano), mes_str)
|
||||
det = logic.obtener_detalle_vendedor("__TODOS__", str(ano), str(mes))
|
||||
pend_filas = det.get("Venta Pendientes", []) if isinstance(det, dict) else []
|
||||
pend_por_vend = {}
|
||||
for f in pend_filas:
|
||||
if not f or str(f[0]).strip().upper() == "TOTAL GENERAL":
|
||||
continue
|
||||
vend = str(f[0]).strip()
|
||||
# INV NETA está en la posición [7] como "S/ x"
|
||||
try:
|
||||
monto = float(str(f[7]).replace("S/", "").replace(",", "").strip())
|
||||
except Exception:
|
||||
@@ -129,6 +113,7 @@ def ventas(ano, mes, sede="TODOS", programa="TODOS", con_overrides=False):
|
||||
acc = pend_por_vend.setdefault(vend, {"monto": 0.0, "cant": 0})
|
||||
acc["monto"] += monto
|
||||
acc["cant"] += 1
|
||||
# Sobrescribir PENDIENTES en cada registro de la tabla principal
|
||||
for d in datos:
|
||||
v = str(d.get("VENDEDOR", "")).strip()
|
||||
acc = pend_por_vend.get(v, {"monto": 0.0, "cant": 0})
|
||||
@@ -138,12 +123,12 @@ def ventas(ano, mes, sede="TODOS", programa="TODOS", con_overrides=False):
|
||||
print(f"[ventas] no se pudo alinear PENDIENTES con el Ver: {e}")
|
||||
|
||||
filas = logic.formatear_datos_para_tabla(datos)
|
||||
print(f"[DEBUG Ventas] Proceso completado. Registros encontrados: {len(datos)}")
|
||||
return {
|
||||
"filas": _serializar_filas(filas),
|
||||
"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, con_overrides), _load)
|
||||
|
||||
|
||||
def _clasif_filtro_programa(dm, dscp):
|
||||
"""Misma clasificación de programa que ejecutar_consulta_ventas (SEMINARIOS/TEAC/TERC/OTROS)."""
|
||||
@@ -155,6 +140,7 @@ def _clasif_filtro_programa(dm, dscp):
|
||||
return cat
|
||||
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 _load():
|
||||
dm = get_dm()
|
||||
@@ -164,7 +150,8 @@ def ventas_detalle(vendedor, ano, mes, tipo_lista, sede="TODOS", programa="TODOS
|
||||
resultado = logic.obtener_detalle_vendedor(vendedor, str(ano), str(mes))
|
||||
filas = resultado.get(tipo_lista, []) if isinstance(resultado, dict) else []
|
||||
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()
|
||||
_prog = str(programa or "TODOS").upper()
|
||||
if _sede != "TODOS" or _prog != "TODOS":
|
||||
@@ -176,6 +163,8 @@ def ventas_detalle(vendedor, ano, mes, tipo_lista, sede="TODOS", programa="TODOS
|
||||
out.append(f); continue
|
||||
if _sede != "TODOS" and str(f[14]).strip().upper() != _sede:
|
||||
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]
|
||||
if _prog != "TODOS" and _clasif_filtro_programa(dm, _prog_nombre) != _prog:
|
||||
continue
|
||||
@@ -184,7 +173,10 @@ def ventas_detalle(vendedor, ano, mes, tipo_lista, sede="TODOS", programa="TODOS
|
||||
return {"filas": _serializar_filas(filas)}
|
||||
return cache_get_or_set("ventas_det", (vendedor, ano, mes, tipo_lista, sede, programa), _load)
|
||||
|
||||
|
||||
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():
|
||||
dm = get_dm()
|
||||
logic = VentasLogic(dm)
|
||||
@@ -193,7 +185,9 @@ def comisiones_detalle_todos(ano, mes):
|
||||
return {"filas": _serializar_filas(filas)}
|
||||
return cache_get_or_set("comisiones_det_todos", ("GLOBAL",), _load)
|
||||
|
||||
|
||||
def comisiones_overrides_fecha():
|
||||
"""Devuelve dict {num_matricula: fecha_cancelacion1_override} para clasificación."""
|
||||
try:
|
||||
sb = _sb()
|
||||
res = sb.table("comisiones_overrides").select("num_matricula,fecha_cancelacion1").execute()
|
||||
@@ -202,6 +196,8 @@ def comisiones_overrides_fecha():
|
||||
mat = str(r["num_matricula"])
|
||||
v = r.get("fecha_cancelacion1")
|
||||
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__":
|
||||
out[mat] = "__VACIO__"
|
||||
elif sv != "":
|
||||
@@ -210,7 +206,10 @@ def comisiones_overrides_fecha():
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
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:
|
||||
sb = _sb()
|
||||
res = sb.table("comisiones_overrides").select("num_matricula,inversion_neta").execute()
|
||||
@@ -222,14 +221,19 @@ def comisiones_overrides_inv_neta():
|
||||
s = str(v).replace("S/", "").replace(",", "").strip()
|
||||
if s == "":
|
||||
continue
|
||||
try: out[str(r["num_matricula"])] = float(s)
|
||||
try:
|
||||
out[str(r["num_matricula"])] = float(s)
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
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:
|
||||
dm = get_dm()
|
||||
if not getattr(dm, "supabase_client", None):
|
||||
@@ -238,6 +242,7 @@ def _aplicar_overrides_comisiones(filas):
|
||||
ov = {str(r["num_matricula"]): r for r in (res.data or [])}
|
||||
if not ov:
|
||||
return filas
|
||||
# columna override -> índice en la fila
|
||||
mapa = {
|
||||
"fch_matricula": 2, "promedio_cuota": 5, "inversion_neta": 7,
|
||||
"fecha_inicio": 9, "saldo_matricula": 10, "saldo_cuota1": 11,
|
||||
@@ -248,8 +253,10 @@ def _aplicar_overrides_comisiones(filas):
|
||||
if len(f) < 16:
|
||||
continue
|
||||
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:
|
||||
f.append(f[8])
|
||||
f.append(f[8]) # buscador: no trae [16] → reutiliza el nombre mostrado
|
||||
if len(f) == 17:
|
||||
f.append("0")
|
||||
if mat in ov:
|
||||
@@ -259,59 +266,72 @@ def _aplicar_overrides_comisiones(filas):
|
||||
if val is None:
|
||||
continue
|
||||
if str(val) == "__VACIO__":
|
||||
f[idx] = ""
|
||||
f[idx] = "" # forzar vacío explícito (override "borrar")
|
||||
elif str(val) != "":
|
||||
f[idx] = val
|
||||
f[17] = "1"
|
||||
f[17] = "1" # marcar fila como editada
|
||||
return filas
|
||||
except Exception as e:
|
||||
print(f"[overrides comisiones] {e}")
|
||||
return filas
|
||||
|
||||
|
||||
# ── COBRANZA ───────────────────────────────────────────────────────────────
|
||||
def cobranza(ano, mes, sectorista="TODOS", agrupacion="SEDE"):
|
||||
def cobranza(ano, mes, sectorista="TODOS", agrupacion="SEDE", sede_filtro="TODOS", prog_filtro="TODOS", venc_hasta="TODOS"):
|
||||
def _load():
|
||||
dm = get_dm()
|
||||
logic = CobranzaLogic(dm)
|
||||
print(f"[DEBUG Cobranza] Buscando ano={ano}, mes={mes}, sectorista={sectorista}, agrupacion={agrupacion}")
|
||||
|
||||
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}")
|
||||
filas = logic.obtener_datos_tabla(str(ano), str(mes), sectorista, agrupacion, sede_filtro, prog_filtro, venc_hasta)
|
||||
sectoristas = logic.obtener_lista_sectoristas(str(ano), str(mes))
|
||||
return {
|
||||
"filas": _serializar_filas(filas),
|
||||
"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, sede_filtro, prog_filtro, venc_hasta), _load)
|
||||
|
||||
def cobranza_detalle(grupo, ano, mes, sectorista, agrupacion="PROGRAMA"):
|
||||
|
||||
def cobranza_detalle(grupo, ano, mes, sectorista, agrupacion="PROGRAMA", prog_filtro="TODOS", sede_filtro="TODOS", venc_hasta="TODOS"):
|
||||
def _load():
|
||||
dm = get_dm()
|
||||
logic = CobranzaLogic(dm)
|
||||
proc = logic.processor
|
||||
pf = str(prog_filtro).upper()
|
||||
sf = str(sede_filtro).upper()
|
||||
vh = venc_hasta
|
||||
# Mapa MATRICULA -> programa, para filtrar por tipo de programa y/o sede (REPLACE_META)
|
||||
mats_prog = None
|
||||
if pf not in ("TODOS", "", "NONE") or sf not in ("TODOS", "", "NONE"):
|
||||
crudos_pf = proc.obtener_detalle_programa(str(ano), str(mes), sectorista, "TODOS")
|
||||
mats_prog = set()
|
||||
for d in (crudos_pf or []):
|
||||
prog = d.get("PROGRAMA", "") or d.get("dsc_programa", "")
|
||||
ok_prog = pf in ("TODOS", "", "NONE") or str(proc.clasificar_filtro_programa(prog)).upper() == pf
|
||||
ok_sede = sf in ("TODOS", "", "NONE") or str(proc.clasificar_sede_meta(prog)).upper() == sf
|
||||
if ok_prog and ok_sede:
|
||||
mats_prog.add(str(d.get("MATRICULA", "")))
|
||||
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", vh)
|
||||
elif agrupacion == "SEDE":
|
||||
crudos = logic.processor.obtener_detalle_programa(str(ano), str(mes), sectorista, "TODOS")
|
||||
# Filtrar por sede usando el MISMO clasificador del processor (sede.json)
|
||||
crudos = proc.obtener_detalle_programa(str(ano), str(mes), sectorista, "TODOS")
|
||||
grupo_up = str(grupo).upper()
|
||||
mats_sede = set()
|
||||
for d in (crudos or []):
|
||||
prog = d.get("PROGRAMA", "") or d.get("dsc_programa", "")
|
||||
sede_det = str(logic.processor.clasificar_sede(prog)).upper()
|
||||
sede_det = str(proc.clasificar_sede(prog)).upper()
|
||||
if sede_det == grupo_up:
|
||||
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", vh)
|
||||
# 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]
|
||||
else:
|
||||
datos = logic.obtener_detalle_programa_formateado(str(ano), str(mes), sectorista, grupo)
|
||||
else: # PROGRAMA
|
||||
datos = logic.obtener_detalle_programa_formateado(str(ano), str(mes), sectorista, grupo, vh)
|
||||
# Filtro por Sede y/o tipo de programa (TEAC/TERC/...) si aplica
|
||||
if mats_prog is not None:
|
||||
datos = [f for f in (datos or []) if str(f[0]) in mats_prog or str(f[0]).upper() == "TOTAL"]
|
||||
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, prog_filtro, sede_filtro, venc_hasta), _load)
|
||||
|
||||
|
||||
# ── RENTABILIDAD ───────────────────────────────────────────────────────────
|
||||
def rentabilidad(ano, mes, sede="TODOS", filtro_prog="TODOS"):
|
||||
@@ -325,6 +345,7 @@ def rentabilidad(ano, mes, sede="TODOS", filtro_prog="TODOS"):
|
||||
return {"filas": filas_s, "programas": nombres}
|
||||
return cache_get_or_set("rentabilidad", (ano, mes, sede, filtro_prog), _load)
|
||||
|
||||
|
||||
def rentabilidad_detalle(programa, ano, mes):
|
||||
def _load():
|
||||
dm = get_dm()
|
||||
@@ -333,6 +354,7 @@ def rentabilidad_detalle(programa, ano, mes):
|
||||
return {"filas": _serializar_filas(resultado)}
|
||||
return cache_get_or_set("rentabilidad_det", (programa, ano, mes), _load)
|
||||
|
||||
|
||||
def rentabilidad_costos(programa, ano, mes):
|
||||
def _load():
|
||||
dm = get_dm()
|
||||
@@ -340,6 +362,7 @@ def rentabilidad_costos(programa, ano, mes):
|
||||
return {"costos": logic.obtener_datos_costos_programa(programa, str(ano), str(mes))}
|
||||
return cache_get_or_set("rentabilidad_costos", (programa, ano, mes), _load)
|
||||
|
||||
|
||||
# ── SALDO PENDIENTE ────────────────────────────────────────────────────────
|
||||
def saldo_pendiente(tipo_cuota):
|
||||
def _load():
|
||||
@@ -349,7 +372,8 @@ def saldo_pendiente(tipo_cuota):
|
||||
return {"datos": _serializar_dicts(datos)}
|
||||
return cache_get_or_set("saldo", (tipo_cuota,), _load)
|
||||
|
||||
# ── PRECARGA GLOBAL ────────────────────────────────────────────────────────
|
||||
|
||||
# ── PRECARGA GLOBAL (calienta todo el caché) ───────────────────────────────
|
||||
def precargar_todo():
|
||||
from datetime import datetime
|
||||
ahora = datetime.now()
|
||||
@@ -372,13 +396,16 @@ def precargar_todo():
|
||||
# Precargar mes actual + anterior de TODOS los módulos (sin entrar a ellos)
|
||||
precargar_todos_modulos()
|
||||
|
||||
|
||||
_refresh_count = 0
|
||||
|
||||
def _recalcular_clave(key):
|
||||
"""Dada una clave 'prefijo:arg1|arg2|...', recalcula su valor sin usar el caché viejo."""
|
||||
from cache_manager import cache_invalidate
|
||||
try:
|
||||
prefijo, _, resto = key.partition(":")
|
||||
args = resto.split("|") if resto else []
|
||||
# Invalidar SOLO esta entrada para forzar su recálculo
|
||||
cache_invalidate(prefijo + ":" + resto if resto else prefijo)
|
||||
if prefijo == "ocupabilidad" and len(args) == 4:
|
||||
return ocupabilidad(args[0], args[1], args[2], args[3])
|
||||
@@ -396,18 +423,24 @@ def _recalcular_clave(key):
|
||||
return cobranza_detalle_todos(args[0], args[1], args[2])
|
||||
except Exception as e:
|
||||
print(f"[recalcular] {key}: {e}")
|
||||
return None
|
||||
return None # los detalles puntuales se recalculan al pedirse
|
||||
|
||||
|
||||
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
|
||||
from cache_manager import cache_refresh_existing, cache_keys
|
||||
_refresh_count += 1
|
||||
# El hilo corre cada 900s (15 min); 4 ciclos ≈ 60 min → recargar config GitHub
|
||||
if _refresh_count % 4 == 0:
|
||||
try:
|
||||
get_dm().cargar_toda_configuracion()
|
||||
print("[config] Recarga de queries/JSON de GitHub completada")
|
||||
except Exception as e:
|
||||
print(f"[config] error al recargar GitHub: {e}")
|
||||
|
||||
# Si el caché está vacío (primer arranque), hacer precarga normal
|
||||
if not cache_keys():
|
||||
precargar_todo()
|
||||
else:
|
||||
@@ -424,25 +457,31 @@ def refrescar_todo():
|
||||
|
||||
def refrescar_manual(clave_actual=None):
|
||||
"""Refresco a demanda (botón 'Actualizar'):
|
||||
1) VACÍA el caché y RE-LEE las bases para la vista que el usuario ve AHORA.
|
||||
2) El resto de vistas cacheadas se recalculan en SEGUNDO PLANO.
|
||||
1) VACÍA el caché y RE-LEE las bases (SQL/Postgre/Supabase) para la vista que el
|
||||
usuario está viendo AHORA (clave_actual) → se devuelve al instante.
|
||||
2) El resto de vistas cacheadas se recalculan en SEGUNDO PLANO (hilo aparte),
|
||||
sin bloquear la respuesta.
|
||||
NO afecta al ciclo automático de 15 min."""
|
||||
import threading
|
||||
from cache_manager import cache_keys, cache_invalidate
|
||||
|
||||
claves_previas = cache_keys()
|
||||
# Vaciar TODO el caché para forzar re-lectura real de las bases
|
||||
cache_invalidate()
|
||||
# También el caché anual de cobranza (si no, el recálculo usaría datos viejos)
|
||||
try:
|
||||
get_dm().limpiar_cache_cobranza()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 1) Vista actual primero (bloqueante, rápido)
|
||||
if clave_actual:
|
||||
try:
|
||||
_recalcular_clave(clave_actual)
|
||||
except Exception as e:
|
||||
print(f"[refresh manual] vista actual {clave_actual}: {e}")
|
||||
|
||||
# 2) El resto en segundo plano
|
||||
resto = [k for k in claves_previas if k != clave_actual]
|
||||
|
||||
def _fondo():
|
||||
@@ -455,21 +494,27 @@ def refrescar_manual(clave_actual=None):
|
||||
print(f"[refresh manual] segundo plano completado ({len(resto)} vistas)")
|
||||
|
||||
threading.Thread(target=_fondo, daemon=True).start()
|
||||
# Reprecargar mes actual + anterior de todos los módulos con datos frescos
|
||||
precargar_todos_modulos()
|
||||
_marcar_actualizacion()
|
||||
return ultima_actualizacion()
|
||||
|
||||
|
||||
# ── PRECARGA EN SEGUNDO PLANO (cache warming por módulo) ────────────────────
|
||||
# Calienta en 2º plano TODAS las combinaciones de la TABLA PRINCIPAL del año
|
||||
# indicado, de a una y con micro-pausas para NO saturar las bases. Solo lo que
|
||||
# consulta al backend; los filtros que se resuelven en el cliente (sede/frecuencia
|
||||
# en Cobranza) quedan instantáneos sin precargar nada. Los detalles (Ver/buscar)
|
||||
# NO se precargan. Cada llamada nueva reemplaza (supersede) la precarga anterior.
|
||||
import threading as _threading
|
||||
_precarga_gen = {}
|
||||
_precarga_estado = {}
|
||||
_precarga_gen = {} # modulo -> id de generación (para cancelar la anterior)
|
||||
_precarga_estado = {} # modulo -> {"total","hechas","completo","ano"}
|
||||
_precarga_lock = _threading.Lock()
|
||||
_PRECARGA_PAUSA = 0.15
|
||||
_PRECARGA_PAUSA = 0.15 # seg entre vistas, para no golpear las bases
|
||||
|
||||
|
||||
def precarga_estado(modulo):
|
||||
"""Progreso de la precarga de un módulo."""
|
||||
"""Progreso de la precarga de un módulo (para el aviso en el frontend)."""
|
||||
modulo = (modulo or "").lower()
|
||||
with _precarga_lock:
|
||||
e = _precarga_estado.get(modulo)
|
||||
@@ -485,11 +530,13 @@ def _periodos_act_ant():
|
||||
|
||||
|
||||
def _combos_cobranza(ano):
|
||||
"""(ano, mes, sectorista, agrupacion) a calentar: SOLO mes actual y anterior."""
|
||||
"""Lista de (ano, mes, sectorista, agrupacion) a calentar para Cobranza.
|
||||
SOLO el mes actual y el mes anterior (no todo el año)."""
|
||||
periodos = _periodos_act_ant()
|
||||
agrupaciones = ["SEDE", "PROGRAMA", "ASESOR"]
|
||||
combos = []
|
||||
for (yy, mm) in periodos:
|
||||
# sectoristas reales de ese mes (+ TODOS). Si falla, solo TODOS.
|
||||
try:
|
||||
logic = CobranzaLogic(get_dm())
|
||||
secs = logic.obtener_lista_sectoristas(str(yy), str(mm)) or ["TODOS"]
|
||||
@@ -503,7 +550,9 @@ def _combos_cobranza(ano):
|
||||
|
||||
|
||||
def precargar_modulo(modulo, ano, en_hilo=True):
|
||||
"""Precarga la tabla principal del módulo (mes actual + anterior)."""
|
||||
"""Precarga la tabla principal del módulo para el año (mes actual + anterior).
|
||||
Si en_hilo=True lanza un hilo y devuelve al instante; si False corre sincrónico
|
||||
(para encadenar varios módulos en un solo hilo, sin ráfagas de conexiones)."""
|
||||
modulo = (modulo or "").lower()
|
||||
with _precarga_lock:
|
||||
gen = _precarga_gen.get(modulo, 0) + 1
|
||||
@@ -536,9 +585,9 @@ def precargar_modulo(modulo, ano, en_hilo=True):
|
||||
_set_total(len(combos))
|
||||
for (yy, mm, sec, ag) in combos:
|
||||
if not _vigente():
|
||||
return
|
||||
return # una precarga más nueva la reemplazó
|
||||
try:
|
||||
cobranza(str(yy), str(mm), sec, ag)
|
||||
cobranza(str(yy), str(mm), sec, ag) # cache_get_or_set: si ya está, no reconsulta
|
||||
_sumar("hechas")
|
||||
except Exception as e:
|
||||
print(f"[precarga cobranza] {yy}-{mm} {ag}/{sec}: {e}")
|
||||
@@ -547,6 +596,7 @@ def precargar_modulo(modulo, ano, en_hilo=True):
|
||||
_marcar_completo()
|
||||
print(f"[precarga cobranza] {ano} completado ({len(combos)} vistas)")
|
||||
elif modulo in ("ocupabilidad", "ventas", "rentabilidad"):
|
||||
# Solo mes actual y anterior, con filtros por defecto (TODOS/TODOS)
|
||||
periodos = _periodos_act_ant()
|
||||
_set_total(len(periodos))
|
||||
for (yy, mm) in periodos:
|
||||
@@ -578,7 +628,8 @@ def precargar_modulo(modulo, ano, en_hilo=True):
|
||||
|
||||
def precargar_todos_modulos():
|
||||
"""Precarga (en 2º plano) el mes actual y anterior de TODOS los módulos, de forma
|
||||
SECUENCIAL en un solo hilo. Se llama al arrancar, cada 15 min y tras el refresco manual."""
|
||||
SECUENCIAL en un solo hilo para no saturar el SQL. Se llama al arrancar, en cada
|
||||
ciclo de 15 min y tras el refresco manual. No requiere entrar a cada módulo."""
|
||||
from datetime import datetime
|
||||
ano = datetime.now().year
|
||||
|
||||
@@ -593,7 +644,7 @@ def precargar_todos_modulos():
|
||||
_threading.Thread(target=_todo, daemon=True).start()
|
||||
|
||||
|
||||
# ── OPERACIONES EXTRA ──────────────────────────────────────────────────────
|
||||
# ── GUARDAR COSTOS (override Supabase) ─────────────────────────────────────
|
||||
def guardar_costos(num_indice, costos_inicial, costos_actual):
|
||||
dm = get_dm()
|
||||
ok1 = dm.guardar_override_costo(num_indice, "inicial", costos_inicial)
|
||||
@@ -603,12 +654,30 @@ def guardar_costos(num_indice, costos_inicial, costos_actual):
|
||||
cache_invalidate("rentabilidad_costos")
|
||||
return bool(ok1 and ok2)
|
||||
|
||||
|
||||
# ── CLASIFICACIÓN DE SEDE (sede.json) ──────────────────────────────────────
|
||||
def clasificar_programas(nombres):
|
||||
"""Devuelve {programa: sede} usando el clasificador del processor (sede.json)."""
|
||||
dm = get_dm()
|
||||
logic = CobranzaLogic(dm)
|
||||
return {n: str(logic.processor.clasificar_sede(n)) for n in nombres}
|
||||
|
||||
|
||||
def clasificar_programas_full(nombres):
|
||||
"""Devuelve {programa: {sede, filtro}} usando el JSON REPLACE_META (meta_data):
|
||||
- sede: clasificacion_sedes (LIMA/PIURA/AREQUIPA/TRUJILLO)
|
||||
- filtro: clasificacion_filtro_programa (TEAC/TERC/SEMINARIOS/OTROS)."""
|
||||
dm = get_dm()
|
||||
logic = RentabilidadLogic(dm) # su identificar_sede/identificar_filtro_programa usan meta_data
|
||||
out = {}
|
||||
for n in nombres:
|
||||
out[n] = {"sede": str(logic.identificar_sede(n)),
|
||||
"filtro": str(logic.identificar_filtro_programa(n))}
|
||||
return out
|
||||
|
||||
|
||||
def cobranza_detalle_todos(ano, mes, sectorista="TODOS"):
|
||||
"""Trae TODOS los alumnos en UNA sola consulta (para export y buscador)."""
|
||||
def _load():
|
||||
dm = get_dm()
|
||||
logic = CobranzaLogic(dm)
|
||||
@@ -636,7 +705,8 @@ def cobranza_detalle_todos(ano, mes, sectorista="TODOS"):
|
||||
return {"alumnos": alumnos}
|
||||
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():
|
||||
dm = get_dm()
|
||||
if not dm.supabase_client:
|
||||
@@ -716,13 +786,17 @@ def usuarios_eliminar(user_id):
|
||||
sb.auth.admin.delete_user(user_id)
|
||||
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):
|
||||
"""Lista la config de un mes (todas las filas por tipo_programa)."""
|
||||
sb = _sb()
|
||||
res = sb.table("comisiones_config_mensual").select("*").eq("ano", int(ano)).eq("mes", int(mes)).execute()
|
||||
return {"config": res.data or []}
|
||||
|
||||
def comisiones_config_guardar(ano, mes, filas):
|
||||
"""Reemplaza la config del mes. filas = [{tipo_programa, imp_tc, meta, comision}]."""
|
||||
sb = _sb()
|
||||
sb.table("comisiones_config_mensual").delete().eq("ano", int(ano)).eq("mes", int(mes)).execute()
|
||||
payload = []
|
||||
@@ -742,6 +816,7 @@ def comisiones_config_guardar(ano, mes, filas):
|
||||
return {"status": "ok"}
|
||||
|
||||
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:
|
||||
sb = _sb()
|
||||
res = sb.table("comisiones_config_mensual").select("imp_tc").eq("ano", int(ano)).eq("mes", int(mes)).execute()
|
||||
@@ -752,10 +827,11 @@ def comisiones_tc_del_mes(ano, mes):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def vendedores_manuales_listar(ano, mes):
|
||||
sb = _sb()
|
||||
query = sb.table("vendedores_manuales").select("*").eq("ano", int(ano))
|
||||
if int(mes) != 0:
|
||||
if int(mes) != 0: # mes=0 → traer todo el año (para cache en frontend)
|
||||
query = query.eq("mes", int(mes))
|
||||
res = query.execute()
|
||||
return {"vendedores": res.data or []}
|
||||
@@ -775,6 +851,7 @@ def vendedores_manuales_eliminar(id):
|
||||
sb.table("vendedores_manuales").delete().eq("id", id).execute()
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
def vendedores_manuales_actualizar(id, descripcion, fch_emision, monto):
|
||||
sb = _sb()
|
||||
sb.table("vendedores_manuales").update({
|
||||
@@ -783,9 +860,14 @@ def vendedores_manuales_actualizar(id, descripcion, fch_emision, monto):
|
||||
}).eq("id", id).execute()
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
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()
|
||||
# Borrar las existentes de ese vendedor/mes
|
||||
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 = []
|
||||
for f in filas:
|
||||
payload.append({
|
||||
@@ -799,8 +881,12 @@ def vendedores_manuales_guardar_lote(nombre, ano, mes, filas):
|
||||
sb.table("vendedores_manuales").insert(payload).execute()
|
||||
return {"status": "ok"}
|
||||
|
||||
# ── OVERRIDES DE COMISIONES ────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
# ── OVERRIDES DE COMISIONES (editar valores por num_matricula) ─────────────
|
||||
def comisiones_override_guardar(registros):
|
||||
"""Guarda/actualiza overrides en UNA sola operación (batch upsert)."""
|
||||
sb = _sb()
|
||||
from cache_manager import cache_invalidate
|
||||
validos = [r for r in registros if r.get("num_matricula")]
|
||||
@@ -812,6 +898,7 @@ def comisiones_override_guardar(registros):
|
||||
return {"status": "ok"}
|
||||
|
||||
def comisiones_override_restaurar(num_matricula):
|
||||
"""Borra el override de un alumno (vuelve a los valores de SQL)."""
|
||||
sb = _sb()
|
||||
from cache_manager import cache_invalidate
|
||||
sb.table("comisiones_overrides").delete().eq("num_matricula", str(num_matricula)).execute()
|
||||
|
||||
Reference in New Issue
Block a user