Actualizar backend/services.py

This commit is contained in:
2026-07-10 14:14:34 -05:00
parent 0e0909f230
commit 1a051760d4

View File

@@ -13,6 +13,18 @@ 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
# Hora de la última actualización de DATOS (se setea al precargar/refrescar)
_ultima_actualizacion = None
def _marcar_actualizacion():
global _ultima_actualizacion
from datetime import datetime
_ultima_actualizacion = datetime.now()
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: def get_dm() -> DataManager:
global _DM global _DM
if _DM is None: if _DM is None:
@@ -356,6 +368,9 @@ def precargar_todo():
t() t()
except Exception as e: except Exception as e:
print(f"[precarga] error: {e}") print(f"[precarga] error: {e}")
_marcar_actualizacion()
# Precargar mes actual + anterior de TODOS los módulos (sin entrar a ellos)
precargar_todos_modulos()
_refresh_count = 0 _refresh_count = 0
@@ -369,6 +384,8 @@ def _recalcular_clave(key):
return ocupabilidad(args[0], args[1], args[2], args[3]) return ocupabilidad(args[0], args[1], args[2], args[3])
if prefijo == "ventas" and len(args) == 2: if prefijo == "ventas" and len(args) == 2:
return ventas(args[0], args[1]) return ventas(args[0], args[1])
if prefijo == "ventas" and len(args) == 5:
return ventas(args[0], args[1], args[2], args[3], args[4] == "True")
if prefijo == "cobranza" and len(args) == 4: if prefijo == "cobranza" and len(args) == 4:
return cobranza(args[0], args[1], args[2], args[3]) return cobranza(args[0], args[1], args[2], args[3])
if prefijo == "rentabilidad" and len(args) == 4: if prefijo == "rentabilidad" and len(args) == 4:
@@ -394,7 +411,187 @@ def refrescar_todo():
if not cache_keys(): if not cache_keys():
precargar_todo() precargar_todo()
else: else:
# Limpiar el caché anual de cobranza para que el recálculo lea SQL fresco
try:
get_dm().limpiar_cache_cobranza()
except Exception:
pass
cache_refresh_existing(_recalcular_clave) cache_refresh_existing(_recalcular_clave)
_marcar_actualizacion()
# Reprecargar mes actual + anterior de todos los módulos (incluye combos nuevos)
precargar_todos_modulos()
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.
NO afecta al ciclo automático de 15 min."""
import threading
from cache_manager import cache_keys, cache_invalidate
claves_previas = cache_keys()
cache_invalidate()
try:
get_dm().limpiar_cache_cobranza()
except Exception:
pass
if clave_actual:
try:
_recalcular_clave(clave_actual)
except Exception as e:
print(f"[refresh manual] vista actual {clave_actual}: {e}")
resto = [k for k in claves_previas if k != clave_actual]
def _fondo():
for k in resto:
try:
_recalcular_clave(k)
except Exception as e:
print(f"[refresh manual bg] {k}: {e}")
_marcar_actualizacion()
print(f"[refresh manual] segundo plano completado ({len(resto)} vistas)")
threading.Thread(target=_fondo, daemon=True).start()
precargar_todos_modulos()
_marcar_actualizacion()
return ultima_actualizacion()
# ── PRECARGA EN SEGUNDO PLANO (cache warming por módulo) ────────────────────
import threading as _threading
_precarga_gen = {}
_precarga_estado = {}
_precarga_lock = _threading.Lock()
_PRECARGA_PAUSA = 0.15
def precarga_estado(modulo):
"""Progreso de la precarga de un módulo."""
modulo = (modulo or "").lower()
with _precarga_lock:
e = _precarga_estado.get(modulo)
return dict(e) if e else {"total": 0, "hechas": 0, "fallidas": 0, "completo": False}
def _periodos_act_ant():
"""(año, mes) del mes actual y el anterior (maneja el cambio de año en enero)."""
from datetime import datetime
hoy = datetime.now()
prev = (hoy.year - 1, 12) if hoy.month == 1 else (hoy.year, hoy.month - 1)
return [(hoy.year, hoy.month), prev]
def _combos_cobranza(ano):
"""(ano, mes, sectorista, agrupacion) a calentar: SOLO mes actual y anterior."""
periodos = _periodos_act_ant()
agrupaciones = ["SEDE", "PROGRAMA", "ASESOR"]
combos = []
for (yy, mm) in periodos:
try:
logic = CobranzaLogic(get_dm())
secs = logic.obtener_lista_sectoristas(str(yy), str(mm)) or ["TODOS"]
except Exception:
secs = ["TODOS"]
secs = list(dict.fromkeys(["TODOS"] + [s for s in secs if s and s != "TODOS"]))
for ag in agrupaciones:
for sec in secs:
combos.append((yy, mm, sec, ag))
return combos
def precargar_modulo(modulo, ano, en_hilo=True):
"""Precarga la tabla principal del módulo (mes actual + anterior)."""
modulo = (modulo or "").lower()
with _precarga_lock:
gen = _precarga_gen.get(modulo, 0) + 1
_precarga_gen[modulo] = gen
_precarga_estado[modulo] = {"total": 0, "hechas": 0, "fallidas": 0, "completo": False, "ano": ano}
def _vigente():
with _precarga_lock:
return _precarga_gen.get(modulo) == gen
def _sumar(campo):
with _precarga_lock:
if _precarga_gen.get(modulo) == gen:
_precarga_estado[modulo][campo] += 1
def _set_total(n):
with _precarga_lock:
if _precarga_gen.get(modulo) == gen:
_precarga_estado[modulo]["total"] = n
def _marcar_completo():
with _precarga_lock:
if _precarga_gen.get(modulo) == gen:
_precarga_estado[modulo]["completo"] = True
def _fondo():
import time
if modulo == "cobranza":
combos = _combos_cobranza(ano)
_set_total(len(combos))
for (yy, mm, sec, ag) in combos:
if not _vigente():
return
try:
cobranza(str(yy), str(mm), sec, ag)
_sumar("hechas")
except Exception as e:
print(f"[precarga cobranza] {yy}-{mm} {ag}/{sec}: {e}")
_sumar("fallidas")
time.sleep(_PRECARGA_PAUSA)
_marcar_completo()
print(f"[precarga cobranza] {ano} completado ({len(combos)} vistas)")
elif modulo in ("ocupabilidad", "ventas", "rentabilidad"):
periodos = _periodos_act_ant()
_set_total(len(periodos))
for (yy, mm) in periodos:
if not _vigente():
return
try:
if modulo == "ocupabilidad":
ocupabilidad(str(yy), str(mm), "TODOS", "TODOS")
elif modulo == "ventas":
ventas(str(yy), str(mm), "TODOS", "TODOS")
else:
rentabilidad(str(yy), str(mm), "TODOS", "TODOS")
_sumar("hechas")
except Exception as e:
print(f"[precarga {modulo}] {yy}-{mm}: {e}")
_sumar("fallidas")
time.sleep(_PRECARGA_PAUSA)
_marcar_completo()
print(f"[precarga {modulo}] {ano} completado ({len(periodos)} vistas)")
else:
print(f"[precarga] módulo no soportado aún: {modulo}")
if en_hilo:
_threading.Thread(target=_fondo, daemon=True).start()
else:
_fondo()
return {"status": "precarga iniciada", "modulo": modulo, "ano": ano}
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."""
from datetime import datetime
ano = datetime.now().year
def _todo():
for m in ("cobranza", "ocupabilidad", "ventas", "rentabilidad"):
try:
precargar_modulo(m, ano, en_hilo=False)
except Exception as e:
print(f"[precarga todos] {m}: {e}")
print("[precarga todos] completado (todos los módulos)")
_threading.Thread(target=_todo, daemon=True).start()
# ── OPERACIONES EXTRA ────────────────────────────────────────────────────── # ── OPERACIONES EXTRA ──────────────────────────────────────────────────────
def guardar_costos(num_indice, costos_inicial, costos_actual): def guardar_costos(num_indice, costos_inicial, costos_actual):