From 1a051760d418a583fb330e36e8ebafefc931c4f2 Mon Sep 17 00:00:00 2001 From: Administrator Date: Fri, 10 Jul 2026 14:14:34 -0500 Subject: [PATCH] Actualizar backend/services.py --- backend/services.py | 197 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/backend/services.py b/backend/services.py index fe4000b..a0eceb3 100644 --- a/backend/services.py +++ b/backend/services.py @@ -13,6 +13,18 @@ from cache_manager import cache_get_or_set # Instancia única del DataManager (como @st.cache_resource) _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: global _DM if _DM is None: @@ -356,6 +368,9 @@ def precargar_todo(): t() except Exception as 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 @@ -369,6 +384,8 @@ def _recalcular_clave(key): return ocupabilidad(args[0], args[1], args[2], args[3]) if prefijo == "ventas" and len(args) == 2: 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: return cobranza(args[0], args[1], args[2], args[3]) if prefijo == "rentabilidad" and len(args) == 4: @@ -394,7 +411,187 @@ def refrescar_todo(): if not cache_keys(): precargar_todo() 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) + _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 ────────────────────────────────────────────────────── def guardar_costos(num_indice, costos_inicial, costos_actual):