Actualizar backend/services.py
This commit is contained in:
@@ -1,33 +1,33 @@
|
|||||||
# backend/services.py
|
# backend/services.py
|
||||||
"""Servicios: traen datos crudos (cacheados) y arman la respuesta del módulo Leads."""
|
"""Servicios: traen datos crudos (cacheados) y arman la respuesta del módulo Leads."""
|
||||||
from data_manager_v2 import DataManager
|
from data_manager_v2 import DataManager
|
||||||
import leads_logic as L
|
import leads_logic as L
|
||||||
from cache_manager import cache_get_or_set, cache_invalidate
|
from cache_manager import cache_get_or_set, cache_invalidate
|
||||||
|
|
||||||
_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
|
||||||
|
|
||||||
|
|
||||||
# ── Prefijos de cache que dependen del mapa de campañas (leyendas). Al cambiar
|
# ── Prefijos de cache que dependen del mapa de campañas (leyendas). Al cambiar
|
||||||
# una leyenda se invalidan estos para que TODOS los apartados se reconstruyan
|
# una leyenda se invalidan estos para que TODOS los apartados se reconstruyan
|
||||||
# en su proxima peticion (perezoso: solo lo que se pida, ~2-5s), sin esperar
|
# en su proxima peticion (perezoso: solo lo que se pida, ~2-5s), sin esperar
|
||||||
# los 15 min ni recargar todo. NO se toca la logica, solo se refresca el cache.
|
# los 15 min ni recargar todo. NO se toca la logica, solo se refresca el cache.
|
||||||
_PREF_CAMPANIAS = (
|
_PREF_CAMPANIAS = (
|
||||||
"campanias_map", "campanias_sc", "leads_raw", "leads_dash", "leads_filtros",
|
"campanias_map", "campanias_sc", "leads_raw", "leads_dash", "leads_filtros",
|
||||||
"leads_alertas", "vendedores_dash", "roas_dash", "otros_general",
|
"leads_alertas", "vendedores_dash", "roas_dash", "otros_general",
|
||||||
"matriz_always", "matriz_webform", "matriz_asignados", "matriz_plantillas",
|
"matriz_always", "matriz_webform", "matriz_asignados", "matriz_plantillas",
|
||||||
"tiempo_resp", "importe_pauta_periodo", "resultados_pauta_periodo",
|
"tiempo_resp", "importe_pauta_periodo", "resultados_pauta_periodo",
|
||||||
"resultados_pauta", "leads_x_codigo", "tel_es_copito", "programa_dash",
|
"resultados_pauta", "leads_x_codigo", "tel_es_copito", "programa_dash",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _invalidar_por_campanias():
|
def _invalidar_por_campanias():
|
||||||
"""Invalida el cache de campañas (Supabase) y de todos los apartados que
|
"""Invalida el cache de campañas (Supabase) y de todos los apartados que
|
||||||
dependen de las leyendas, para que el cambio se vea de inmediato."""
|
dependen de las leyendas, para que el cambio se vea de inmediato."""
|
||||||
from data_manager_v2 import invalidar_campanias_cache
|
from data_manager_v2 import invalidar_campanias_cache
|
||||||
@@ -36,31 +36,31 @@ def _invalidar_por_campanias():
|
|||||||
cache_invalidate(p)
|
cache_invalidate(p)
|
||||||
|
|
||||||
|
|
||||||
# ── Datos crudos cacheados (consultas pesadas, una sola vez) ────
|
# ── Datos crudos cacheados (consultas pesadas, una sola vez) ────
|
||||||
def _leads_crudos():
|
def _leads_crudos():
|
||||||
return cache_get_or_set("leads_raw", ("GLOBAL",),
|
return cache_get_or_set("leads_raw", ("GLOBAL",),
|
||||||
lambda: L.procesar_leads(get_dm().traer_leads_chatwoot()))
|
lambda: L.procesar_leads(get_dm().traer_leads_chatwoot()))
|
||||||
|
|
||||||
|
|
||||||
def _cursos_crudos():
|
def _cursos_crudos():
|
||||||
return cache_get_or_set("cursos_raw", ("GLOBAL",), lambda: get_dm().traer_cursos())
|
return cache_get_or_set("cursos_raw", ("GLOBAL",), lambda: get_dm().traer_cursos())
|
||||||
|
|
||||||
|
|
||||||
def _matriculas_crudas():
|
def _matriculas_crudas():
|
||||||
return cache_get_or_set("mat_raw", ("GLOBAL",), lambda: get_dm().traer_matriculas())
|
return cache_get_or_set("mat_raw", ("GLOBAL",), lambda: get_dm().traer_matriculas())
|
||||||
|
|
||||||
|
|
||||||
def _pauta_cruda():
|
def _pauta_cruda():
|
||||||
return cache_get_or_set("pauta_raw", ("GLOBAL",), lambda: get_dm().traer_pauta_cursos())
|
return cache_get_or_set("pauta_raw", ("GLOBAL",), lambda: get_dm().traer_pauta_cursos())
|
||||||
|
|
||||||
|
|
||||||
def _programa_pautas():
|
def _programa_pautas():
|
||||||
"""{num_indice: [pautas]} desde la tabla programa_pautas (muchos-a-muchos)."""
|
"""{num_indice: [pautas]} desde la tabla programa_pautas (muchos-a-muchos)."""
|
||||||
return cache_get_or_set("programa_pautas", ("GLOBAL",),
|
return cache_get_or_set("programa_pautas", ("GLOBAL",),
|
||||||
lambda: get_dm().traer_programa_pautas())
|
lambda: get_dm().traer_programa_pautas())
|
||||||
|
|
||||||
|
|
||||||
def _pautas_de_indice():
|
def _pautas_de_indice():
|
||||||
"""{num_indice: [pautas]} combinando:
|
"""{num_indice: [pautas]} combinando:
|
||||||
- la pauta de basebi_programacion (1 por num_indice), y
|
- la pauta de basebi_programacion (1 por num_indice), y
|
||||||
- las pautas extra de programa_pautas (varias).
|
- las pautas extra de programa_pautas (varias).
|
||||||
@@ -82,18 +82,18 @@ def _pautas_de_indice():
|
|||||||
return cache_get_or_set("pautas_de_indice", ("GLOBAL",), _load)
|
return cache_get_or_set("pautas_de_indice", ("GLOBAL",), _load)
|
||||||
|
|
||||||
|
|
||||||
def _campanias_map():
|
def _campanias_map():
|
||||||
return cache_get_or_set("campanias_map", ("GLOBAL",),
|
return cache_get_or_set("campanias_map", ("GLOBAL",),
|
||||||
lambda: get_dm().traer_campanias_map())
|
lambda: get_dm().traer_campanias_map())
|
||||||
|
|
||||||
|
|
||||||
def _clasif_meta():
|
def _clasif_meta():
|
||||||
"""JSON de clasificacion (sede/programa) de GitHub, MISMO que Ventas."""
|
"""JSON de clasificacion (sede/programa) de GitHub, MISMO que Ventas."""
|
||||||
return cache_get_or_set("clasif_meta", ("GLOBAL",),
|
return cache_get_or_set("clasif_meta", ("GLOBAL",),
|
||||||
lambda: get_dm().traer_clasificacion_meta())
|
lambda: get_dm().traer_clasificacion_meta())
|
||||||
|
|
||||||
|
|
||||||
def clasificar_sede_meta(dsc_programa):
|
def clasificar_sede_meta(dsc_programa):
|
||||||
"""Sede segun el JSON (clasificacion_sedes). Default LIMA. Igual que Ventas."""
|
"""Sede segun el JSON (clasificacion_sedes). Default LIMA. Igual que Ventas."""
|
||||||
up = str(dsc_programa or "").upper()
|
up = str(dsc_programa or "").upper()
|
||||||
data = _clasif_meta().get("clasificacion_sedes", {})
|
data = _clasif_meta().get("clasificacion_sedes", {})
|
||||||
@@ -106,7 +106,7 @@ def clasificar_sede_meta(dsc_programa):
|
|||||||
return data.get("DEFAULT", "LIMA")
|
return data.get("DEFAULT", "LIMA")
|
||||||
|
|
||||||
|
|
||||||
def clasificar_prog_meta(dsc_programa):
|
def clasificar_prog_meta(dsc_programa):
|
||||||
"""Programa (TEAC/TERC/SEMINARIOS/OTROS) segun clasificacion_filtro_programa
|
"""Programa (TEAC/TERC/SEMINARIOS/OTROS) segun clasificacion_filtro_programa
|
||||||
del JSON. Respeta el ORDEN del JSON (primero que coincide gana). Igual que Ventas."""
|
del JSON. Respeta el ORDEN del JSON (primero que coincide gana). Igual que Ventas."""
|
||||||
up = str(dsc_programa or "").upper()
|
up = str(dsc_programa or "").upper()
|
||||||
@@ -120,7 +120,7 @@ def clasificar_prog_meta(dsc_programa):
|
|||||||
return data.get("DEFAULT", "OTROS")
|
return data.get("DEFAULT", "OTROS")
|
||||||
|
|
||||||
|
|
||||||
def meta_de_curso(dsc_programa):
|
def meta_de_curso(dsc_programa):
|
||||||
"""Meta de inscritos segun sede+programa (JSON), como acordado:
|
"""Meta de inscritos segun sede+programa (JSON), como acordado:
|
||||||
LIMA TEAC/TERC=22, provincia TEAC/TERC=18, SEMINARIOS/OTROS=15."""
|
LIMA TEAC/TERC=22, provincia TEAC/TERC=18, SEMINARIOS/OTROS=15."""
|
||||||
sede = clasificar_sede_meta(dsc_programa)
|
sede = clasificar_sede_meta(dsc_programa)
|
||||||
@@ -130,22 +130,22 @@ def meta_de_curso(dsc_programa):
|
|||||||
return 15
|
return 15
|
||||||
|
|
||||||
|
|
||||||
# Conecta los clasificadores del JSON a leads_logic (metas + tipo de matricula).
|
# Conecta los clasificadores del JSON a leads_logic (metas + tipo de matricula).
|
||||||
L.set_clasificadores(clasificar_sede_meta, clasificar_prog_meta)
|
L.set_clasificadores(clasificar_sede_meta, clasificar_prog_meta)
|
||||||
|
|
||||||
|
|
||||||
def _conjunto_pauta():
|
def _conjunto_pauta():
|
||||||
return cache_get_or_set("conjunto_pauta", ("GLOBAL",),
|
return cache_get_or_set("conjunto_pauta", ("GLOBAL",),
|
||||||
lambda: get_dm().traer_conjunto_pauta())
|
lambda: get_dm().traer_conjunto_pauta())
|
||||||
|
|
||||||
|
|
||||||
def _meta_filas():
|
def _meta_filas():
|
||||||
"""Filas crudas del CSV de Meta: [{conjunto, importe, inicio}, ...]."""
|
"""Filas crudas del CSV de Meta: [{conjunto, importe, inicio}, ...]."""
|
||||||
return cache_get_or_set("meta_filas", ("GLOBAL",),
|
return cache_get_or_set("meta_filas", ("GLOBAL",),
|
||||||
lambda: get_dm().traer_meta_importe())
|
lambda: get_dm().traer_meta_importe())
|
||||||
|
|
||||||
|
|
||||||
def _importe_por_pauta():
|
def _importe_por_pauta():
|
||||||
"""{pauta: importe_gastado_total} = suma del importe de TODAS las filas de los
|
"""{pauta: importe_gastado_total} = suma del importe de TODAS las filas de los
|
||||||
conjuntos ligados a esa pauta (conjunto_pauta × Meta CSV). Sin filtro de fecha."""
|
conjuntos ligados a esa pauta (conjunto_pauta × Meta CSV). Sin filtro de fecha."""
|
||||||
def _load():
|
def _load():
|
||||||
@@ -162,11 +162,11 @@ def _importe_por_pauta():
|
|||||||
return cache_get_or_set("importe_pauta", ("GLOBAL",), _load)
|
return cache_get_or_set("importe_pauta", ("GLOBAL",), _load)
|
||||||
|
|
||||||
|
|
||||||
# Canales que cuentan como Copito (ademas de origen_base=POSTGRE).
|
# Canales que cuentan como Copito (ademas de origen_base=POSTGRE).
|
||||||
_CANALES_COPITO = {"PAUTA_WSP", "PAUTA_WSP_FACE", "COPITO"}
|
_CANALES_COPITO = {"PAUTA_WSP", "PAUTA_WSP_FACE", "COPITO"}
|
||||||
|
|
||||||
|
|
||||||
def _tel_es_copito():
|
def _tel_es_copito():
|
||||||
"""{telefono: True/False} = si el telefono es COPITO segun su FILA MAS ANTIGUA
|
"""{telefono: True/False} = si el telefono es COPITO segun su FILA MAS ANTIGUA
|
||||||
en la cartera. Copito = (origen_base=POSTGRE) OR (canal en _CANALES_COPITO).
|
en la cartera. Copito = (origen_base=POSTGRE) OR (canal en _CANALES_COPITO).
|
||||||
Se calcula una vez sobre toda la cartera (cacheado)."""
|
Se calcula una vez sobre toda la cartera (cacheado)."""
|
||||||
@@ -188,7 +188,7 @@ def _tel_es_copito():
|
|||||||
return cache_get_or_set("tel_es_copito", ("GLOBAL",), _load)
|
return cache_get_or_set("tel_es_copito", ("GLOBAL",), _load)
|
||||||
|
|
||||||
|
|
||||||
def _leads_por_codigo_periodo(ano, mes, dia):
|
def _leads_por_codigo_periodo(ano, mes, dia):
|
||||||
"""{codigo(pauta): {"rec": set(tels recibidos del periodo),
|
"""{codigo(pauta): {"rec": set(tels recibidos del periodo),
|
||||||
"proc": set(tels procesados del periodo)}}.
|
"proc": set(tels procesados del periodo)}}.
|
||||||
Recibido = lead unico con ese codigo cuya fecha cae en el periodo.
|
Recibido = lead unico con ese codigo cuya fecha cae en el periodo.
|
||||||
@@ -215,7 +215,7 @@ def _leads_por_codigo_periodo(ano, mes, dia):
|
|||||||
return cache_get_or_set("leads_x_codigo", (ano, mes, dia), _load)
|
return cache_get_or_set("leads_x_codigo", (ano, mes, dia), _load)
|
||||||
|
|
||||||
|
|
||||||
def _resultados_por_pauta():
|
def _resultados_por_pauta():
|
||||||
"""{pauta: resultados_totales} = suma de Resultados de TODAS las filas de los
|
"""{pauta: resultados_totales} = suma de Resultados de TODAS las filas de los
|
||||||
conjuntos ligados a esa pauta. Sin filtro de fecha (analogo a _importe_por_pauta)."""
|
conjuntos ligados a esa pauta. Sin filtro de fecha (analogo a _importe_por_pauta)."""
|
||||||
def _load():
|
def _load():
|
||||||
@@ -232,7 +232,7 @@ def _resultados_por_pauta():
|
|||||||
return cache_get_or_set("resultados_pauta", ("GLOBAL",), _load)
|
return cache_get_or_set("resultados_pauta", ("GLOBAL",), _load)
|
||||||
|
|
||||||
|
|
||||||
def _importe_por_pauta_periodo(ano, mes, dia):
|
def _importe_por_pauta_periodo(ano, mes, dia):
|
||||||
"""{pauta: importe} sumando solo las filas del CSV cuyo 'Inicio del informe'
|
"""{pauta: importe} sumando solo las filas del CSV cuyo 'Inicio del informe'
|
||||||
cae dentro del periodo filtrado (año/mes/día). Respeta el filtro de arriba."""
|
cae dentro del periodo filtrado (año/mes/día). Respeta el filtro de arriba."""
|
||||||
def _load():
|
def _load():
|
||||||
@@ -255,7 +255,7 @@ def _importe_por_pauta_periodo(ano, mes, dia):
|
|||||||
return cache_get_or_set("importe_pauta_periodo", (ano, mes, dia), _load)
|
return cache_get_or_set("importe_pauta_periodo", (ano, mes, dia), _load)
|
||||||
|
|
||||||
|
|
||||||
def _resultados_por_pauta_periodo(ano, mes, dia):
|
def _resultados_por_pauta_periodo(ano, mes, dia):
|
||||||
"""{pauta: resultados} sumando solo las filas del CSV cuyo 'Inicio del informe'
|
"""{pauta: resultados} sumando solo las filas del CSV cuyo 'Inicio del informe'
|
||||||
cae en el periodo filtrado. Analogo a _importe_por_pauta_periodo pero con Resultados."""
|
cae en el periodo filtrado. Analogo a _importe_por_pauta_periodo pero con Resultados."""
|
||||||
def _load():
|
def _load():
|
||||||
@@ -275,12 +275,12 @@ def _resultados_por_pauta_periodo(ano, mes, dia):
|
|||||||
return cache_get_or_set("resultados_pauta_periodo", (ano, mes, dia), _load)
|
return cache_get_or_set("resultados_pauta_periodo", (ano, mes, dia), _load)
|
||||||
|
|
||||||
|
|
||||||
def _campanias_sede_cargo():
|
def _campanias_sede_cargo():
|
||||||
return cache_get_or_set("campanias_sc", ("GLOBAL",),
|
return cache_get_or_set("campanias_sc", ("GLOBAL",),
|
||||||
lambda: get_dm().traer_campanias_sede_cargo())
|
lambda: get_dm().traer_campanias_sede_cargo())
|
||||||
|
|
||||||
|
|
||||||
def matriz_always(ano="TODOS", mes="TODOS", dia="TODOS", sede="TODOS", programa="TODOS"):
|
def matriz_always(ano="TODOS", mes="TODOS", dia="TODOS", sede="TODOS", programa="TODOS"):
|
||||||
"""Matriz de gasto Meta Ads de pautas ALWAYS (con gasto pero SIN num_indice).
|
"""Matriz de gasto Meta Ads de pautas ALWAYS (con gasto pero SIN num_indice).
|
||||||
Agrupa Sede -> Programa (TEAC/TERC/SEMINARIOS) con Importe, Resultados,
|
Agrupa Sede -> Programa (TEAC/TERC/SEMINARIOS) con Importe, Resultados,
|
||||||
Leads Nuevos (fecha_creada en el mes) y Leads Nuevos Asignados (con asesor).
|
Leads Nuevos (fecha_creada en el mes) y Leads Nuevos Asignados (con asesor).
|
||||||
@@ -426,7 +426,7 @@ def matriz_always(ano="TODOS", mes="TODOS", dia="TODOS", sede="TODOS", programa=
|
|||||||
return cache_get_or_set("matriz_always", (ano, mes, dia, sede, programa), _load)
|
return cache_get_or_set("matriz_always", (ano, mes, dia, sede, programa), _load)
|
||||||
|
|
||||||
|
|
||||||
def matriz_web_formulario(ano="TODOS", mes="TODOS", dia="TODOS", sede="TODOS", programa="TODOS"):
|
def matriz_web_formulario(ano="TODOS", mes="TODOS", dia="TODOS", sede="TODOS", programa="TODOS"):
|
||||||
"""Matriz de leads con canal WEB_FORMULARIO (cartera_junta), agrupados Sede -> Programa.
|
"""Matriz de leads con canal WEB_FORMULARIO (cartera_junta), agrupados Sede -> Programa.
|
||||||
Valores: Leads Recibidos (todos), Leads Procesados (con asesor), Matriculas y
|
Valores: Leads Recibidos (todos), Leads Procesados (con asesor), Matriculas y
|
||||||
Valor Venta (cruce por telefono, fch_matricula >= fecha_creada del lead)."""
|
Valor Venta (cruce por telefono, fch_matricula >= fecha_creada del lead)."""
|
||||||
@@ -521,23 +521,23 @@ def matriz_web_formulario(ano="TODOS", mes="TODOS", dia="TODOS", sede="TODOS", p
|
|||||||
return cache_get_or_set("matriz_webform", (ano, mes, dia, sede, programa), _load)
|
return cache_get_or_set("matriz_webform", (ano, mes, dia, sede, programa), _load)
|
||||||
|
|
||||||
|
|
||||||
def _asignacion_respuesta_crudos():
|
def _asignacion_respuesta_crudos():
|
||||||
return cache_get_or_set("asig_resp", ("GLOBAL",),
|
return cache_get_or_set("asig_resp", ("GLOBAL",),
|
||||||
lambda: get_dm().traer_asignacion_respuesta())
|
lambda: get_dm().traer_asignacion_respuesta())
|
||||||
|
|
||||||
|
|
||||||
def _leads_asignados_crudos():
|
def _leads_asignados_crudos():
|
||||||
# UNIFICADO: usa la MISMA consulta/cache que el grafico (asignacion+respuesta+etiquetas),
|
# UNIFICADO: usa la MISMA consulta/cache que el grafico (asignacion+respuesta+etiquetas),
|
||||||
# asi la tarjeta, la matriz de asignados y el grafico nunca se desfasan.
|
# asi la tarjeta, la matriz de asignados y el grafico nunca se desfasan.
|
||||||
return _asignacion_respuesta_crudos()
|
return _asignacion_respuesta_crudos()
|
||||||
|
|
||||||
|
|
||||||
def _actividad_asesores_crudos():
|
def _actividad_asesores_crudos():
|
||||||
return cache_get_or_set("act_asesores", ("GLOBAL",),
|
return cache_get_or_set("act_asesores", ("GLOBAL",),
|
||||||
lambda: get_dm().traer_actividad_asesores())
|
lambda: get_dm().traer_actividad_asesores())
|
||||||
|
|
||||||
|
|
||||||
def matriz_asignados(ano="TODOS", mes="TODOS", dia="TODOS"):
|
def matriz_asignados(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||||
"""Matriz pivot: filas = asesor (user_name) con sus telefonos; columnas = dias del
|
"""Matriz pivot: filas = asesor (user_name) con sus telefonos; columnas = dias del
|
||||||
mes; valor = cantidad de telefonos asignados a ese asesor ese dia.
|
mes; valor = cantidad de telefonos asignados a ese asesor ese dia.
|
||||||
Fecha = created_at del mensaje de asignacion (ya con -5h)."""
|
Fecha = created_at del mensaje de asignacion (ya con -5h)."""
|
||||||
@@ -584,12 +584,12 @@ def matriz_asignados(ano="TODOS", mes="TODOS", dia="TODOS"):
|
|||||||
return cache_get_or_set("matriz_asignados", (ano, mes, dia), _load)
|
return cache_get_or_set("matriz_asignados", (ano, mes, dia), _load)
|
||||||
|
|
||||||
|
|
||||||
def _plantillas_crudas():
|
def _plantillas_crudas():
|
||||||
return cache_get_or_set("plantillas_raw", ("GLOBAL",),
|
return cache_get_or_set("plantillas_raw", ("GLOBAL",),
|
||||||
lambda: get_dm().traer_plantillas())
|
lambda: get_dm().traer_plantillas())
|
||||||
|
|
||||||
|
|
||||||
def matriz_plantillas(ano="TODOS", mes="TODOS", dia="TODOS"):
|
def matriz_plantillas(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||||
"""Matriz de plantillas COBRADAS por plantilla. Columnas:
|
"""Matriz de plantillas COBRADAS por plantilla. Columnas:
|
||||||
ENVIADAS = Mas_de_24h=SI, created_at_peru en el periodo filtrado
|
ENVIADAS = Mas_de_24h=SI, created_at_peru en el periodo filtrado
|
||||||
RESPONDIDAS= de esas, con respuesta del contacto (siguiente_mensaje_1)
|
RESPONDIDAS= de esas, con respuesta del contacto (siguiente_mensaje_1)
|
||||||
@@ -683,13 +683,13 @@ def matriz_plantillas(ano="TODOS", mes="TODOS", dia="TODOS"):
|
|||||||
return cache_get_or_set("matriz_plantillas", (ano, mes, dia), _load)
|
return cache_get_or_set("matriz_plantillas", (ano, mes, dia), _load)
|
||||||
|
|
||||||
|
|
||||||
def _norm_ni(x):
|
def _norm_ni(x):
|
||||||
"""Normaliza num_indice a texto (quita el '.0' si viniera como float)."""
|
"""Normaliza num_indice a texto (quita el '.0' si viniera como float)."""
|
||||||
s = str(x or "").strip()
|
s = str(x or "").strip()
|
||||||
return s[:-2] if s.endswith(".0") else s
|
return s[:-2] if s.endswith(".0") else s
|
||||||
|
|
||||||
|
|
||||||
def _mat_por_indice():
|
def _mat_por_indice():
|
||||||
"""{num_indice: [(telefono_resuelto, fecha_matricula), ...]} desde las matrículas.
|
"""{num_indice: [(telefono_resuelto, fecha_matricula), ...]} desde las matrículas.
|
||||||
Teléfono resuelto = dsc_telefono_1 si existe; si no, dsc_telefono_2."""
|
Teléfono resuelto = dsc_telefono_1 si existe; si no, dsc_telefono_2."""
|
||||||
def _load():
|
def _load():
|
||||||
@@ -705,14 +705,14 @@ def _mat_por_indice():
|
|||||||
return cache_get_or_set("mat_por_indice", ("GLOBAL",), _load)
|
return cache_get_or_set("mat_por_indice", ("GLOBAL",), _load)
|
||||||
|
|
||||||
|
|
||||||
def _cartera_rows_cache():
|
def _cartera_rows_cache():
|
||||||
"""Filas crudas de cartera_junta cacheadas (se bajan de Supabase UNA sola vez).
|
"""Filas crudas de cartera_junta cacheadas (se bajan de Supabase UNA sola vez).
|
||||||
Evita re-descargar las 86k filas en cada llamada/cambio de mes."""
|
Evita re-descargar las 86k filas en cada llamada/cambio de mes."""
|
||||||
return cache_get_or_set("cartera_rows", ("GLOBAL",),
|
return cache_get_or_set("cartera_rows", ("GLOBAL",),
|
||||||
lambda: get_dm().traer_cartera_rows())
|
lambda: get_dm().traer_cartera_rows())
|
||||||
|
|
||||||
|
|
||||||
def _cartera_idx():
|
def _cartera_idx():
|
||||||
"""Devuelve {'all':..., 'asig':...}, cada uno {(sede,programa):{telefono:fecha_min}}.
|
"""Devuelve {'all':..., 'asig':...}, cada uno {(sede,programa):{telefono:fecha_min}}.
|
||||||
'all' = todos los teléfonos; 'asig' = solo los que tienen asesor (no vacío).
|
'all' = todos los teléfonos; 'asig' = solo los que tienen asesor (no vacío).
|
||||||
Se baja la cartera UNA vez y queda cacheado."""
|
Se baja la cartera UNA vez y queda cacheado."""
|
||||||
@@ -783,8 +783,8 @@ def _cartera_idx():
|
|||||||
return cache_get_or_set("cartera_idx", ("GLOBAL",), _load)
|
return cache_get_or_set("cartera_idx", ("GLOBAL",), _load)
|
||||||
|
|
||||||
|
|
||||||
# ── Respuesta del dashboard de Leads (con filtros) ──────────────
|
# ── Respuesta del dashboard de Leads (con filtros) ──────────────
|
||||||
def leads_dashboard(ano="TODOS", mes="TODOS", dia="TODOS", programa="TODOS", sede="TODOS"):
|
def leads_dashboard(ano="TODOS", mes="TODOS", dia="TODOS", programa="TODOS", sede="TODOS"):
|
||||||
def _load():
|
def _load():
|
||||||
leads = _leads_crudos()
|
leads = _leads_crudos()
|
||||||
cursos = _cursos_crudos()
|
cursos = _cursos_crudos()
|
||||||
@@ -840,18 +840,18 @@ def leads_dashboard(ano="TODOS", mes="TODOS", dia="TODOS", programa="TODOS", sed
|
|||||||
return cache_get_or_set("leads_dash", (ano, mes, dia, programa, sede), _load)
|
return cache_get_or_set("leads_dash", (ano, mes, dia, programa, sede), _load)
|
||||||
|
|
||||||
|
|
||||||
def _asesores():
|
def _asesores():
|
||||||
"""{'map': {alias_norm: correcto}, 'lista': [los 8 vendedores]}."""
|
"""{'map': {alias_norm: correcto}, 'lista': [los 8 vendedores]}."""
|
||||||
return cache_get_or_set("asesores", ("GLOBAL",), lambda: get_dm().traer_asesores())
|
return cache_get_or_set("asesores", ("GLOBAL",), lambda: get_dm().traer_asesores())
|
||||||
|
|
||||||
|
|
||||||
def _norm_asesor(nombre, mapa):
|
def _norm_asesor(nombre, mapa):
|
||||||
"""Normaliza un nombre de asesor a su 'correcto' via el diccionario."""
|
"""Normaliza un nombre de asesor a su 'correcto' via el diccionario."""
|
||||||
k = " ".join(str(nombre or "").upper().split())
|
k = " ".join(str(nombre or "").upper().split())
|
||||||
return mapa.get(k) # None si no es uno de los vendedores
|
return mapa.get(k) # None si no es uno de los vendedores
|
||||||
|
|
||||||
|
|
||||||
def _norm_tel_pe(v):
|
def _norm_tel_pe(v):
|
||||||
"""Normaliza telefono igual que la cartera: quita simbolos y el '51' inicial
|
"""Normaliza telefono igual que la cartera: quita simbolos y el '51' inicial
|
||||||
de celulares peruanos (rango 51900000000-51999999999). Asi cruzan con cartera_junta."""
|
de celulares peruanos (rango 51900000000-51999999999). Asi cruzan con cartera_junta."""
|
||||||
s = str(v or "")
|
s = str(v or "")
|
||||||
@@ -863,7 +863,7 @@ def _norm_tel_pe(v):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
def tiempo_respuesta_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
def tiempo_respuesta_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||||
"""Tiempo de respuesta LABORAL (minutos) del asesor tras la asignacion.
|
"""Tiempo de respuesta LABORAL (minutos) del asesor tras la asignacion.
|
||||||
- Turno detectado por dia/asesor segun donde hay MAS actividad:
|
- Turno detectado por dia/asesor segun donde hay MAS actividad:
|
||||||
tarde (13-18) -> Turno 1 (09-18, refrigerio 13-14 excluido).
|
tarde (13-18) -> Turno 1 (09-18, refrigerio 13-14 excluido).
|
||||||
@@ -1081,7 +1081,7 @@ def tiempo_respuesta_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
|||||||
return cache_get_or_set("tiempo_resp", (ano, mes, dia), _load)
|
return cache_get_or_set("tiempo_resp", (ano, mes, dia), _load)
|
||||||
|
|
||||||
|
|
||||||
def tiempo_respuesta_detalle(dia_click, modo="asignacion", vendedor="TODOS",
|
def tiempo_respuesta_detalle(dia_click, modo="asignacion", vendedor="TODOS",
|
||||||
ano="TODOS", mes="TODOS"):
|
ano="TODOS", mes="TODOS"):
|
||||||
"""Detalle de los leads que forman el promedio de UN día del gráfico
|
"""Detalle de los leads que forman el promedio de UN día del gráfico
|
||||||
'Tiempo de Respuesta por Día'. Reproduce la MISMA lógica de turnos/minutos.
|
'Tiempo de Respuesta por Día'. Reproduce la MISMA lógica de turnos/minutos.
|
||||||
@@ -1222,7 +1222,7 @@ def tiempo_respuesta_detalle(dia_click, modo="asignacion", vendedor="TODOS",
|
|||||||
return cache_get_or_set("tiempo_resp_det", (dia_click, modo, vendedor, ano, mes), _load)
|
return cache_get_or_set("tiempo_resp_det", (dia_click, modo, vendedor, ano, mes), _load)
|
||||||
|
|
||||||
|
|
||||||
def vendedores_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
def vendedores_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||||
"""Por vendedor (los 8 de alias_normalizacion). Filtro Año/Mes/Día:
|
"""Por vendedor (los 8 de alias_normalizacion). Filtro Año/Mes/Día:
|
||||||
- Cartera (total/copito/otros): filtra por fecha_creada de cartera_junta.
|
- Cartera (total/copito/otros): filtra por fecha_creada de cartera_junta.
|
||||||
- Matriculas: se cruzan por TELEFONO con los leads del mes (es_origen=SI,
|
- Matriculas: se cruzan por TELEFONO con los leads del mes (es_origen=SI,
|
||||||
@@ -1454,17 +1454,17 @@ def vendedores_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
|||||||
return cache_get_or_set("vendedores_dash", (ano, mes, dia), _load)
|
return cache_get_or_set("vendedores_dash", (ano, mes, dia), _load)
|
||||||
|
|
||||||
|
|
||||||
def _conjunto_sede_prog():
|
def _conjunto_sede_prog():
|
||||||
"""{conjunto_UPPER: {sede, programa}} asignaciones manuales (tabla Supabase)."""
|
"""{conjunto_UPPER: {sede, programa}} asignaciones manuales (tabla Supabase)."""
|
||||||
return cache_get_or_set("conjunto_sede_prog", ("GLOBAL",),
|
return cache_get_or_set("conjunto_sede_prog", ("GLOBAL",),
|
||||||
lambda: get_dm().traer_conjunto_sede_programa())
|
lambda: get_dm().traer_conjunto_sede_programa())
|
||||||
|
|
||||||
|
|
||||||
# Programas cuyos RESULTADOS no deben sumar (son de otro tipo: alcance/marca).
|
# Programas cuyos RESULTADOS no deben sumar (son de otro tipo: alcance/marca).
|
||||||
_PROG_SIN_RESULTADO = {"BRANDING", "COPEAIRE"}
|
_PROG_SIN_RESULTADO = {"BRANDING", "COPEAIRE"}
|
||||||
|
|
||||||
|
|
||||||
def _conjuntos_branding():
|
def _conjuntos_branding():
|
||||||
"""Set de CONJUNTOS (UPPER) asignados manualmente como BRANDING/COPEAIRE.
|
"""Set de CONJUNTOS (UPPER) asignados manualmente como BRANDING/COPEAIRE.
|
||||||
Sus resultados NO deben contarse (su importe sí)."""
|
Sus resultados NO deben contarse (su importe sí)."""
|
||||||
def _load():
|
def _load():
|
||||||
@@ -1474,7 +1474,7 @@ def _conjuntos_branding():
|
|||||||
return cache_get_or_set("conjuntos_branding", ("GLOBAL",), _load)
|
return cache_get_or_set("conjuntos_branding", ("GLOBAL",), _load)
|
||||||
|
|
||||||
|
|
||||||
def _codigos_branding():
|
def _codigos_branding():
|
||||||
"""Set de codigos(pauta) cuyos conjuntos estan asignados manualmente como
|
"""Set de codigos(pauta) cuyos conjuntos estan asignados manualmente como
|
||||||
BRANDING/COPEAIRE. Sirve para NO contar sus resultados en las tablas."""
|
BRANDING/COPEAIRE. Sirve para NO contar sus resultados en las tablas."""
|
||||||
def _load():
|
def _load():
|
||||||
@@ -1493,7 +1493,7 @@ def _codigos_branding():
|
|||||||
return cache_get_or_set("codigos_branding", ("GLOBAL",), _load)
|
return cache_get_or_set("codigos_branding", ("GLOBAL",), _load)
|
||||||
|
|
||||||
|
|
||||||
def conjuntos_sin_sede():
|
def conjuntos_sin_sede():
|
||||||
"""Lista los conjuntos del Meta CSV que NO estan ligados a ninguna sede
|
"""Lista los conjuntos del Meta CSV que NO estan ligados a ninguna sede
|
||||||
(ni por pauta->campania, ni por asignacion manual). Para el apartado de
|
(ni por pauta->campania, ni por asignacion manual). Para el apartado de
|
||||||
asignacion. Sin filtro de mes. Incluye la sede/programa YA asignados (si tiene).
|
asignacion. Sin filtro de mes. Incluye la sede/programa YA asignados (si tiene).
|
||||||
@@ -1531,7 +1531,7 @@ def conjuntos_sin_sede():
|
|||||||
return cache_get_or_set("conjuntos_sin_sede", ("GLOBAL",), _load)
|
return cache_get_or_set("conjuntos_sin_sede", ("GLOBAL",), _load)
|
||||||
|
|
||||||
|
|
||||||
def guardar_conjunto_sede(cambios):
|
def guardar_conjunto_sede(cambios):
|
||||||
"""Guarda asignaciones conjunto->sede/programa e invalida cachés dependientes."""
|
"""Guarda asignaciones conjunto->sede/programa e invalida cachés dependientes."""
|
||||||
r = get_dm().guardar_conjunto_sede_programa(cambios)
|
r = get_dm().guardar_conjunto_sede_programa(cambios)
|
||||||
for pref in ("conjunto_sede_prog", "conjuntos_sin_sede", "roas_dash"):
|
for pref in ("conjunto_sede_prog", "conjuntos_sin_sede", "roas_dash"):
|
||||||
@@ -1539,7 +1539,7 @@ def guardar_conjunto_sede(cambios):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def borrar_conjunto_sede(conjunto):
|
def borrar_conjunto_sede(conjunto):
|
||||||
"""Borra la asignacion de un conjunto e invalida cachés dependientes."""
|
"""Borra la asignacion de un conjunto e invalida cachés dependientes."""
|
||||||
r = get_dm().borrar_conjunto_sede_programa(conjunto)
|
r = get_dm().borrar_conjunto_sede_programa(conjunto)
|
||||||
for pref in ("conjunto_sede_prog", "conjuntos_sin_sede", "roas_dash"):
|
for pref in ("conjunto_sede_prog", "conjuntos_sin_sede", "roas_dash"):
|
||||||
@@ -1547,7 +1547,7 @@ def borrar_conjunto_sede(conjunto):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def roas_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
def roas_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||||
"""Matriz ROAS por SEDE (LIMA, PIURA, AREQUIPA, TRUJILLO): Importe Gastado y
|
"""Matriz ROAS por SEDE (LIMA, PIURA, AREQUIPA, TRUJILLO): Importe Gastado y
|
||||||
Resultados del Google Sheet (Meta Ads), filtrado por Año/Mes (Inicio del informe).
|
Resultados del Google Sheet (Meta Ads), filtrado por Año/Mes (Inicio del informe).
|
||||||
Cadena: conjunto -> pauta (conjunto_pauta) -> sede (campanias). Conjuntos sin
|
Cadena: conjunto -> pauta (conjunto_pauta) -> sede (campanias). Conjuntos sin
|
||||||
@@ -1693,10 +1693,10 @@ def roas_dashboard(ano="TODOS", mes="TODOS", dia="TODOS"):
|
|||||||
return cache_get_or_set("roas_dash", (ano, mes, dia), _load)
|
return cache_get_or_set("roas_dash", (ano, mes, dia), _load)
|
||||||
|
|
||||||
|
|
||||||
_CURSOS_EXCLUIDOS = {"1121", "1154"}
|
_CURSOS_EXCLUIDOS = {"1121", "1154"}
|
||||||
|
|
||||||
|
|
||||||
def _curso_excluido(c, pmap):
|
def _curso_excluido(c, pmap):
|
||||||
"""Mismo criterio que 'Detalle por Curso': excluye cursos en la lista fija,
|
"""Mismo criterio que 'Detalle por Curso': excluye cursos en la lista fija,
|
||||||
o con contar=NO / estado=SUSPENDIDO en basebi."""
|
o con contar=NO / estado=SUSPENDIDO en basebi."""
|
||||||
ni = str(c.get("num_indice", "")).strip()
|
ni = str(c.get("num_indice", "")).strip()
|
||||||
@@ -1708,7 +1708,7 @@ def _curso_excluido(c, pmap):
|
|||||||
return info.get("contar") == "NO" or info.get("estado") == "SUSPENDIDO"
|
return info.get("contar") == "NO" or info.get("estado") == "SUSPENDIDO"
|
||||||
|
|
||||||
|
|
||||||
def programa_opciones(ano="TODOS", mes="TODOS"):
|
def programa_opciones(ano="TODOS", mes="TODOS"):
|
||||||
"""Lista de programas (dsc_det_programa) cuyos cursos tienen fch_inicio en el
|
"""Lista de programas (dsc_det_programa) cuyos cursos tienen fch_inicio en el
|
||||||
periodo. Para el selector del apartado PROGRAMA. Ordenada por fch_inicio ASC."""
|
periodo. Para el selector del apartado PROGRAMA. Ordenada por fch_inicio ASC."""
|
||||||
def _load():
|
def _load():
|
||||||
@@ -1731,7 +1731,7 @@ def programa_opciones(ano="TODOS", mes="TODOS"):
|
|||||||
return cache_get_or_set("programa_opciones", (ano, mes), _load)
|
return cache_get_or_set("programa_opciones", (ano, mes), _load)
|
||||||
|
|
||||||
|
|
||||||
def programa_dashboard(ano="TODOS", mes="TODOS", programa="TODOS"):
|
def programa_dashboard(ano="TODOS", mes="TODOS", programa="TODOS"):
|
||||||
"""Datos agregados de UN programa (dsc_det_programa) en el periodo:
|
"""Datos agregados de UN programa (dsc_det_programa) en el periodo:
|
||||||
Cartera Total/Copito/Otros, Leads Recibidos/Procesados/Contactados y %.
|
Cartera Total/Copito/Otros, Leads Recibidos/Procesados/Contactados y %.
|
||||||
- Filtro por fch_inicio del curso para elegir los cursos del programa.
|
- Filtro por fch_inicio del curso para elegir los cursos del programa.
|
||||||
@@ -1778,6 +1778,7 @@ def programa_dashboard(ano="TODOS", mes="TODOS", programa="TODOS"):
|
|||||||
# de las pautas del programa. Telefonos unicos por etiqueta.
|
# de las pautas del programa. Telefonos unicos por etiqueta.
|
||||||
from collections import defaultdict as _dd
|
from collections import defaultdict as _dd
|
||||||
etiq_tels = _dd(set)
|
etiq_tels = _dd(set)
|
||||||
|
etiq_ase_tels = _dd(lambda: _dd(set)) # estado -> asesor -> {telefonos}
|
||||||
cods_set0 = set(cods)
|
cods_set0 = set(cods)
|
||||||
for l in _leads_crudos():
|
for l in _leads_crudos():
|
||||||
if not l.get("es_unico") or not l.get("asesor"):
|
if not l.get("es_unico") or not l.get("asesor"):
|
||||||
@@ -1788,9 +1789,16 @@ def programa_dashboard(ano="TODOS", mes="TODOS", programa="TODOS"):
|
|||||||
if not tel:
|
if not tel:
|
||||||
continue
|
continue
|
||||||
ult = l.get("ultima") or "NO TRABAJADO"
|
ult = l.get("ultima") or "NO TRABAJADO"
|
||||||
|
ase = l.get("asesor") or "SIN ASESOR"
|
||||||
etiq_tels[ult].add(tel)
|
etiq_tels[ult].add(tel)
|
||||||
|
etiq_ase_tels[ult][ase].add(tel)
|
||||||
matriz_etiq = sorted(
|
matriz_etiq = sorted(
|
||||||
[{"estado": k, "cantidad": len(v)} for k, v in etiq_tels.items()],
|
[{"estado": k, "cantidad": len(v),
|
||||||
|
"asesores": sorted(
|
||||||
|
[{"asesor": a, "cantidad": len(t), "telefonos": sorted(t)}
|
||||||
|
for a, t in etiq_ase_tels[k].items()],
|
||||||
|
key=lambda x: -x["cantidad"])}
|
||||||
|
for k, v in etiq_tels.items()],
|
||||||
key=lambda x: -x["cantidad"])
|
key=lambda x: -x["cantidad"])
|
||||||
|
|
||||||
# 3) CARTERA TOTAL: MISMA logica que 'Detalle por Curso'. Por cada curso,
|
# 3) CARTERA TOTAL: MISMA logica que 'Detalle por Curso'. Por cada curso,
|
||||||
@@ -1994,7 +2002,7 @@ def programa_dashboard(ano="TODOS", mes="TODOS", programa="TODOS"):
|
|||||||
return cache_get_or_set("programa_dash", (ano, mes, programa), _load)
|
return cache_get_or_set("programa_dash", (ano, mes, programa), _load)
|
||||||
|
|
||||||
|
|
||||||
def otros_general(ano="TODOS", mes="TODOS", dia="TODOS"):
|
def otros_general(ano="TODOS", mes="TODOS", dia="TODOS"):
|
||||||
"""Solo las 4 matrices de la pagina 'Otros General' (sin KPIs/graficos/tabla_pauta).
|
"""Solo las 4 matrices de la pagina 'Otros General' (sin KPIs/graficos/tabla_pauta).
|
||||||
Mas liviano que leads_dashboard -> filtrar por mes es mas rapido."""
|
Mas liviano que leads_dashboard -> filtrar por mes es mas rapido."""
|
||||||
def _load():
|
def _load():
|
||||||
@@ -2007,12 +2015,12 @@ def otros_general(ano="TODOS", mes="TODOS", dia="TODOS"):
|
|||||||
return cache_get_or_set("otros_general", (ano, mes, dia), _load)
|
return cache_get_or_set("otros_general", (ano, mes, dia), _load)
|
||||||
|
|
||||||
|
|
||||||
def conjuntos_sin_pauta():
|
def conjuntos_sin_pauta():
|
||||||
"""Lista de conjuntos de anuncios sin pauta (para el desplegable del pop-up)."""
|
"""Lista de conjuntos de anuncios sin pauta (para el desplegable del pop-up)."""
|
||||||
return get_dm().traer_conjuntos_sin_pauta()
|
return get_dm().traer_conjuntos_sin_pauta()
|
||||||
|
|
||||||
|
|
||||||
def _pautas_de_curso_ni(ni):
|
def _pautas_de_curso_ni(ni):
|
||||||
"""Pautas (codigos) de un num_indice = pauta de basebi + programa_pautas."""
|
"""Pautas (codigos) de un num_indice = pauta de basebi + programa_pautas."""
|
||||||
cds = set()
|
cds = set()
|
||||||
for p in _pautas_de_indice().get(ni, []):
|
for p in _pautas_de_indice().get(ni, []):
|
||||||
@@ -2023,7 +2031,7 @@ def _pautas_de_curso_ni(ni):
|
|||||||
return cds
|
return cds
|
||||||
|
|
||||||
|
|
||||||
def alertas_dashboard():
|
def alertas_dashboard():
|
||||||
"""ALERTAS: cursos que inician en los proximos 30 dias (con pauta, importe>0).
|
"""ALERTAS: cursos que inician en los proximos 30 dias (con pauta, importe>0).
|
||||||
Estado (CRITICO/ADVERTENCIA/BUEN RUMBO) segun MATRICULAS vs meta ESPERADA por
|
Estado (CRITICO/ADVERTENCIA/BUEN RUMBO) segun MATRICULAS vs meta ESPERADA por
|
||||||
dias que faltan. CPL alto / pocos leads son SENALES (no cambian el estado).
|
dias que faltan. CPL alto / pocos leads son SENALES (no cambian el estado).
|
||||||
@@ -2315,7 +2323,7 @@ def alertas_dashboard():
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def programas_ocultos():
|
def programas_ocultos():
|
||||||
"""Programas con contar=NO en basebi_programacion (ocultos de la matriz).
|
"""Programas con contar=NO en basebi_programacion (ocultos de la matriz).
|
||||||
Cada item: {num_indice, dsc_det_programa, fch_inicio, pauta}."""
|
Cada item: {num_indice, dsc_det_programa, fch_inicio, pauta}."""
|
||||||
pm = _pauta_cruda()
|
pm = _pauta_cruda()
|
||||||
@@ -2337,7 +2345,7 @@ def programas_ocultos():
|
|||||||
return {"filas": filas}
|
return {"filas": filas}
|
||||||
|
|
||||||
|
|
||||||
def encender_programas(num_indices):
|
def encender_programas(num_indices):
|
||||||
"""Pone contar=SI a los num_indice dados en basebi_programacion (los reactiva)."""
|
"""Pone contar=SI a los num_indice dados en basebi_programacion (los reactiva)."""
|
||||||
from cache_manager import cache_invalidate
|
from cache_manager import cache_invalidate
|
||||||
dm = get_dm()
|
dm = get_dm()
|
||||||
@@ -2348,7 +2356,7 @@ def encender_programas(num_indices):
|
|||||||
return {"encendidos": len(num_indices)}
|
return {"encendidos": len(num_indices)}
|
||||||
|
|
||||||
|
|
||||||
def programas_disponibles():
|
def programas_disponibles():
|
||||||
"""Lista de dsc_det_programa de cursos con fch_inicio >= 2026-01-01 (para el
|
"""Lista de dsc_det_programa de cursos con fch_inicio >= 2026-01-01 (para el
|
||||||
desplegable de Programa en la Leyenda). Cada item: {num_indice, dsc_det_programa}."""
|
desplegable de Programa en la Leyenda). Cada item: {num_indice, dsc_det_programa}."""
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@@ -2370,30 +2378,30 @@ def programas_disponibles():
|
|||||||
return {"programas": filas}
|
return {"programas": filas}
|
||||||
|
|
||||||
|
|
||||||
# ── LEYENDA (campanias de Supabase): listar / agregar / borrar ──
|
# ── LEYENDA (campanias de Supabase): listar / agregar / borrar ──
|
||||||
def campanias_listar():
|
def campanias_listar():
|
||||||
return get_dm().listar_campanias()
|
return get_dm().listar_campanias()
|
||||||
|
|
||||||
|
|
||||||
def campania_agregar(datos):
|
def campania_agregar(datos):
|
||||||
res = get_dm().agregar_campania(datos)
|
res = get_dm().agregar_campania(datos)
|
||||||
_invalidar_por_campanias() # la leyenda nueva se refleja de inmediato en todo el dashboard
|
_invalidar_por_campanias() # la leyenda nueva se refleja de inmediato en todo el dashboard
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def campania_editar(cid, datos):
|
def campania_editar(cid, datos):
|
||||||
res = get_dm().editar_campania(cid, datos)
|
res = get_dm().editar_campania(cid, datos)
|
||||||
_invalidar_por_campanias() # el cambio se refleja de inmediato en todo el dashboard
|
_invalidar_por_campanias() # el cambio se refleja de inmediato en todo el dashboard
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def campania_borrar(cid):
|
def campania_borrar(cid):
|
||||||
res = get_dm().borrar_campania(cid)
|
res = get_dm().borrar_campania(cid)
|
||||||
_invalidar_por_campanias()
|
_invalidar_por_campanias()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def leyenda_anuncios():
|
def leyenda_anuncios():
|
||||||
"""Tabla: todos los conjuntos del Sheet con su pauta (si tiene) y el programa
|
"""Tabla: todos los conjuntos del Sheet con su pauta (si tiene) y el programa
|
||||||
(dsc_det_programa) del num_indice que usa esa pauta. Cacheado (usa insumos ya
|
(dsc_det_programa) del num_indice que usa esa pauta. Cacheado (usa insumos ya
|
||||||
en memoria) para que abrir la Leyenda sea rapido."""
|
en memoria) para que abrir la Leyenda sea rapido."""
|
||||||
@@ -2431,7 +2439,7 @@ def leyenda_anuncios():
|
|||||||
return cache_get_or_set("leyenda_anuncios", ("GLOBAL",), _load)
|
return cache_get_or_set("leyenda_anuncios", ("GLOBAL",), _load)
|
||||||
|
|
||||||
|
|
||||||
def guardar_leyenda(cambios):
|
def guardar_leyenda(cambios):
|
||||||
"""cambios = [{conjunto, pauta, num_indice?}].
|
"""cambios = [{conjunto, pauta, num_indice?}].
|
||||||
- conjunto -> pauta: upsert en conjunto_pauta.
|
- conjunto -> pauta: upsert en conjunto_pauta.
|
||||||
- si viene num_indice: conecta ese num_indice a la pauta en basebi_programacion.
|
- si viene num_indice: conecta ese num_indice a la pauta en basebi_programacion.
|
||||||
@@ -2460,7 +2468,7 @@ def guardar_leyenda(cambios):
|
|||||||
return {"guardados": len(cambios)}
|
return {"guardados": len(cambios)}
|
||||||
|
|
||||||
|
|
||||||
def uso_de_pauta(pauta, excluir_num_indice=None):
|
def uso_de_pauta(pauta, excluir_num_indice=None):
|
||||||
"""Verifica si una pauta ya esta en uso. Devuelve los cursos (dsc_det_programa)
|
"""Verifica si una pauta ya esta en uso. Devuelve los cursos (dsc_det_programa)
|
||||||
y conjuntos que la usan (para la advertencia antes de guardar)."""
|
y conjuntos que la usan (para la advertencia antes de guardar)."""
|
||||||
dm = get_dm()
|
dm = get_dm()
|
||||||
@@ -2480,7 +2488,7 @@ def uso_de_pauta(pauta, excluir_num_indice=None):
|
|||||||
"cursos": detalle, "num_indices": nis, "conjuntos": conjuntos}
|
"cursos": detalle, "num_indices": nis, "conjuntos": conjuntos}
|
||||||
|
|
||||||
|
|
||||||
def guardar_edicion_curso(num_indice, pauta, conjunto=None, contar=None):
|
def guardar_edicion_curso(num_indice, pauta, conjunto=None, contar=None):
|
||||||
"""Guarda pauta y contar (SI/NO) en basebi_programacion (upsert) y, si se eligió,
|
"""Guarda pauta y contar (SI/NO) en basebi_programacion (upsert) y, si se eligió,
|
||||||
vincula el conjunto a esa pauta en conjunto_pauta. Invalida SOLO los cachés que
|
vincula el conjunto a esa pauta en conjunto_pauta. Invalida SOLO los cachés que
|
||||||
dependen de pauta/conjunto (NO la cartera, para que la recarga sea rápida)."""
|
dependen de pauta/conjunto (NO la cartera, para que la recarga sea rápida)."""
|
||||||
@@ -2498,7 +2506,7 @@ def guardar_edicion_curso(num_indice, pauta, conjunto=None, contar=None):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def _tipo_de_curso(c):
|
def _tipo_de_curso(c):
|
||||||
"""Categoría de tipo de programa para el filtro (PROGRAMAS/PROVINCIA/SEMINARIOS/OTROS)."""
|
"""Categoría de tipo de programa para el filtro (PROGRAMAS/PROVINCIA/SEMINARIOS/OTROS)."""
|
||||||
dscp = c.get("dsc_programa", "")
|
dscp = c.get("dsc_programa", "")
|
||||||
otros = L.clasificar_tipo_programa(dscp)
|
otros = L.clasificar_tipo_programa(dscp)
|
||||||
@@ -2511,8 +2519,8 @@ def _tipo_de_curso(c):
|
|||||||
return L.tipo_programa_cohorte(sede, cat)
|
return L.tipo_programa_cohorte(sede, cat)
|
||||||
|
|
||||||
|
|
||||||
# ── Listas para los filtros (años, tipos de programa) ───────────
|
# ── Listas para los filtros (años, tipos de programa) ───────────
|
||||||
def opciones_filtros():
|
def opciones_filtros():
|
||||||
def _load():
|
def _load():
|
||||||
cursos = _cursos_crudos()
|
cursos = _cursos_crudos()
|
||||||
anos = sorted({L._to_date(c.get("fch_inicio")).year for c in cursos
|
anos = sorted({L._to_date(c.get("fch_inicio")).year for c in cursos
|
||||||
@@ -2523,14 +2531,14 @@ def opciones_filtros():
|
|||||||
return cache_get_or_set("leads_filtros", ("GLOBAL",), _load)
|
return cache_get_or_set("leads_filtros", ("GLOBAL",), _load)
|
||||||
|
|
||||||
|
|
||||||
# ── Alertas: valores de programa/sede no identificados en cartera_junta ──
|
# ── Alertas: valores de programa/sede no identificados en cartera_junta ──
|
||||||
def alertas():
|
def alertas():
|
||||||
return cache_get_or_set("leads_alertas", ("GLOBAL",),
|
return cache_get_or_set("leads_alertas", ("GLOBAL",),
|
||||||
lambda: get_dm().traer_no_identificados())
|
lambda: get_dm().traer_no_identificados())
|
||||||
|
|
||||||
|
|
||||||
# ── Diagnóstico de cursos (para cuadrar ocupabilidad) ───────────
|
# ── Diagnóstico de cursos (para cuadrar ocupabilidad) ───────────
|
||||||
def debug_cursos(ano, mes):
|
def debug_cursos(ano, mes):
|
||||||
cursos = _cursos_crudos()
|
cursos = _cursos_crudos()
|
||||||
periodo = [c for c in cursos if L._en_periodo(L._to_date(c.get("fch_inicio")), ano, mes, "TODOS")]
|
periodo = [c for c in cursos if L._en_periodo(L._to_date(c.get("fch_inicio")), ano, mes, "TODOS")]
|
||||||
detalle = []
|
detalle = []
|
||||||
@@ -2560,8 +2568,8 @@ def debug_cursos(ano, mes):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── Precarga / refresco ─────────────────────────────────────────
|
# ── Precarga / refresco ─────────────────────────────────────────
|
||||||
def _periodos_actual_anterior():
|
def _periodos_actual_anterior():
|
||||||
"""[(ano, mes), ...] = mes actual y mes anterior (segun fecha de hoy)."""
|
"""[(ano, mes), ...] = mes actual y mes anterior (segun fecha de hoy)."""
|
||||||
from datetime import date
|
from datetime import date
|
||||||
hoy = date.today()
|
hoy = date.today()
|
||||||
@@ -2573,11 +2581,11 @@ def _periodos_actual_anterior():
|
|||||||
return [(str(ano), str(mes)), (str(prev[0]), str(prev[1]))]
|
return [(str(ano), str(mes)), (str(prev[0]), str(prev[1]))]
|
||||||
|
|
||||||
|
|
||||||
import os as _os
|
import os as _os
|
||||||
_LOG_CARGA = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "carga_tiempos.log")
|
_LOG_CARGA = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "carga_tiempos.log")
|
||||||
|
|
||||||
|
|
||||||
def _log_carga(linea, reset=False):
|
def _log_carga(linea, reset=False):
|
||||||
"""Escribe una linea en carga_tiempos.log (lo lee la ventana de tiempos)."""
|
"""Escribe una linea en carga_tiempos.log (lo lee la ventana de tiempos)."""
|
||||||
try:
|
try:
|
||||||
with open(_LOG_CARGA, "w" if reset else "a", encoding="utf-8") as f:
|
with open(_LOG_CARGA, "w" if reset else "a", encoding="utf-8") as f:
|
||||||
@@ -2586,13 +2594,13 @@ def _log_carga(linea, reset=False):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
import threading as _thr
|
import threading as _thr
|
||||||
_TIEMPOS = {} # etiqueta -> segundos (para el resumen final)
|
_TIEMPOS = {} # etiqueta -> segundos (para el resumen final)
|
||||||
_TIEMPOS_LOCK = _thr.Lock()
|
_TIEMPOS_LOCK = _thr.Lock()
|
||||||
_HILOS_LISTOS = set() # {"leads","otros"} — cuando estan los 2, se imprime resumen
|
_HILOS_LISTOS = set() # {"leads","otros"} — cuando estan los 2, se imprime resumen
|
||||||
|
|
||||||
|
|
||||||
def _cron(etiqueta, base, fn, apartado=None):
|
def _cron(etiqueta, base, fn, apartado=None):
|
||||||
"""Ejecuta fn() midiendo el tiempo. Imprime y ademas guarda en el log:
|
"""Ejecuta fn() midiendo el tiempo. Imprime y ademas guarda en el log:
|
||||||
paso, base de datos y segundos. Se ve en la ventana del BACKEND y en la
|
paso, base de datos y segundos. Se ve en la ventana del BACKEND y en la
|
||||||
ventana de TIEMPOS al iniciar, para saber que carga cada cosa.
|
ventana de TIEMPOS al iniciar, para saber que carga cada cosa.
|
||||||
@@ -2615,7 +2623,7 @@ def _cron(etiqueta, base, fn, apartado=None):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def _hilo_termino(nombre):
|
def _hilo_termino(nombre):
|
||||||
"""Marca un hilo como listo. Cuando ambos ("leads" y "otros") terminan,
|
"""Marca un hilo como listo. Cuando ambos ("leads" y "otros") terminan,
|
||||||
imprime el resumen final por apartado en el log."""
|
imprime el resumen final por apartado en el log."""
|
||||||
with _TIEMPOS_LOCK:
|
with _TIEMPOS_LOCK:
|
||||||
@@ -2639,7 +2647,7 @@ def _hilo_termino(nombre):
|
|||||||
_log_carga("=" * 60)
|
_log_carga("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
def precargar_todo():
|
def precargar_todo():
|
||||||
"""Precarga en segundo plano: insumos crudos + el dashboard del mes actual y
|
"""Precarga en segundo plano: insumos crudos + el dashboard del mes actual y
|
||||||
anterior (con filtros en TODOS), para que el usuario no espere al abrir/filtrar."""
|
anterior (con filtros en TODOS), para que el usuario no espere al abrir/filtrar."""
|
||||||
try:
|
try:
|
||||||
@@ -2677,7 +2685,7 @@ def precargar_todo():
|
|||||||
_hilo_termino("leads")
|
_hilo_termino("leads")
|
||||||
|
|
||||||
|
|
||||||
def precargar_otros_general():
|
def precargar_otros_general():
|
||||||
"""Precarga (aparte, mas pesada) las 4 matrices de Otros General del mes actual
|
"""Precarga (aparte, mas pesada) las 4 matrices de Otros General del mes actual
|
||||||
y anterior. Se llama en su propio hilo para no bloquear."""
|
y anterior. Se llama en su propio hilo para no bloquear."""
|
||||||
try:
|
try:
|
||||||
@@ -2697,26 +2705,29 @@ def precargar_otros_general():
|
|||||||
_hilo_termino("otros")
|
_hilo_termino("otros")
|
||||||
|
|
||||||
|
|
||||||
# Hora de la última actualización (hora Perú)
|
# Hora de la última actualización (hora Perú)
|
||||||
_ULTIMA_ACTUALIZACION = None
|
_ULTIMA_ACTUALIZACION = None
|
||||||
|
|
||||||
|
|
||||||
def _marcar_actualizacion():
|
def _marcar_actualizacion():
|
||||||
global _ULTIMA_ACTUALIZACION
|
global _ULTIMA_ACTUALIZACION
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
peru = timezone(timedelta(hours=-5))
|
peru = timezone(timedelta(hours=-5))
|
||||||
_ULTIMA_ACTUALIZACION = datetime.now(peru).strftime("%d/%m/%Y %H:%M")
|
_ULTIMA_ACTUALIZACION = datetime.now(peru).strftime("%d/%m/%Y %H:%M")
|
||||||
|
|
||||||
|
|
||||||
def ultima_actualizacion():
|
def ultima_actualizacion():
|
||||||
return {"hora": _ULTIMA_ACTUALIZACION}
|
return {"hora": _ULTIMA_ACTUALIZACION}
|
||||||
|
|
||||||
|
|
||||||
def refrescar_todo():
|
def refrescar_todo():
|
||||||
from cache_manager import cache_invalidate
|
from cache_manager import set_forzar
|
||||||
from data_manager_v2 import invalidar_campanias_cache
|
from data_manager_v2 import invalidar_campanias_cache
|
||||||
cache_invalidate() # vaciar y recargar TODO
|
|
||||||
invalidar_campanias_cache() # tambien las campañas (cache aparte)
|
invalidar_campanias_cache() # tambien las campañas (cache aparte)
|
||||||
|
set_forzar(True) # recalcula y sobrescribe (no lee cache viejo)
|
||||||
|
try:
|
||||||
precargar_todo() # leads (mes actual/anterior)
|
precargar_todo() # leads (mes actual/anterior)
|
||||||
precargar_otros_general() # cartera + Otros General (para que no quede vacia)
|
precargar_otros_general() # cartera + Otros General
|
||||||
|
finally:
|
||||||
|
set_forzar(False)
|
||||||
_marcar_actualizacion()
|
_marcar_actualizacion()
|
||||||
|
|||||||
Reference in New Issue
Block a user