Actualizar backend/leads_logic.py

This commit is contained in:
2026-09-04 16:21:38 -05:00
parent 7953979794
commit 1cf244be56

View File

@@ -1,4 +1,14 @@
# backend/leads_logic.py
# Ganchos de clasificacion por JSON (los setea services.py al arrancar). Si estan
# definidos, meta_curso/tipo_programa_curso los usan; si no, cae al metodo viejo.
_CLASIF_SEDE = None # funcion(dsc_programa) -> sede
_CLASIF_PROG = None # funcion(dsc_programa) -> TEAC/TERC/SEMINARIOS/OTROS
def set_clasificadores(f_sede, f_prog):
global _CLASIF_SEDE, _CLASIF_PROG
_CLASIF_SEDE = f_sede
_CLASIF_PROG = f_prog
"""
Lógica del módulo LEADS. Traduce las medidas DAX del PBI a Python.
Columnas calculadas replicadas:
@@ -97,6 +107,11 @@ def grupo_programa_curso(dsc_programa):
SEMINARIOS = SEMINARIOS
OTROS = OTROS + CARRERA"""
tp = tipo_programa_curso(dsc_programa)
# Con el clasificador por JSON, tipo_programa_curso ya devuelve las 4 categorias
# simples (TEAC/TERC/SEMINARIOS/OTROS) -> se devuelven tal cual.
if tp in ("TEAC", "TERC", "SEMINARIOS", "OTROS"):
return tp
# Compatibilidad con el metodo viejo (nombres detallados sin JSON).
if tp in ("PROGRAMAS TEAC", "PROVINCIA TEAC"):
return "TEAC"
if tp in ("PROGRAMAS TERC", "PROVINCIA TERC"):
@@ -162,8 +177,10 @@ def tipo_programa_cohorte(sede, programa_cat):
def tipo_programa_curso(dsc_programa):
"""Réplica EXACTA del DAX Tipo_Programa (columna calculada de Fact_SQL_Base_Cursos).
Usa CONTAINSSTRING en el mismo ORDEN que el PBI (el orden importa)."""
"""Tipo de programa. Si hay clasificador por JSON, devuelve directo las 4
categorias simples (TEAC/TERC/SEMINARIOS/OTROS), igual que Ocupabilidad."""
if _CLASIF_PROG:
return _CLASIF_PROG(dsc_programa) # TEAC / TERC / SEMINARIOS / OTROS
p = str(dsc_programa or "")
def has(s): # CONTAINSSTRING es sensible a may/min en DAX; comparamos tal cual
return s in p
@@ -215,8 +232,15 @@ def tipo_programa_curso(dsc_programa):
def meta_curso(dsc_programa):
"""Meta por curso según Tipo_Programa (igual que el DAX Meta_Curso):
PROGRAMAS TEAC/TERC=22, PROVINCIA TEAC/TERC=18, resto=15."""
"""Meta por curso: LIMA TEAC/TERC=22, PROVINCIA TEAC/TERC=18, resto=15.
Usa la clasificacion del JSON (sede+programa) si esta disponible."""
if _CLASIF_SEDE and _CLASIF_PROG:
sede = _CLASIF_SEDE(dsc_programa)
prog = _CLASIF_PROG(dsc_programa)
if prog in ("TEAC", "TERC"):
return 22 if sede == "LIMA" else 18
return 15
# respaldo: metodo viejo
tp = tipo_programa_curso(dsc_programa)
if tp in ("PROGRAMAS TEAC", "PROGRAMAS TERC"):
return 22
@@ -474,10 +498,14 @@ def leads_por_dia(leads, ano, mes, dia):
# ── Tabla por Sede_Act -> Tipo_Programa con 4 medidas (cohorte de leads) ──
def tabla_pauta(leads, matriculas, ano, mes, dia, importe_por_pauta_periodo=None,
resultados_por_pauta_periodo=None, campanias_sede_cargo=None):
resultados_por_pauta_periodo=None, campanias_sede_cargo=None,
codigos_branding=None):
importe_por_pauta_periodo = importe_por_pauta_periodo or {}
resultados_por_pauta_periodo = resultados_por_pauta_periodo or {}
campanias_sede_cargo = campanias_sede_cargo or {}
# Codigos (pautas) de BRANDING/COPEAIRE: su importe suma normal, pero sus
# resultados NO se cuentan (se ponen en 0).
codigos_branding = codigos_branding or set()
# 1) Leads únicos del periodo con su teléfono, fecha, sede y tipo
lp = [l for l in leads if l["es_unico"] and _en_periodo(l["fecha"], ano, mes, dia)]
# mapa teléfono -> fecha del lead (para "matrícula posterior al lead")
@@ -541,6 +569,8 @@ def tabla_pauta(leads, matriculas, ano, mes, dia, importe_por_pauta_periodo=None
continue
se = str(info.get("sede") or "").upper()
tp = grupo_tipo_cohorte(tipo_programa_cohorte(se, info.get("cargo")))
# Guarda el resultado REAL (para mostrarlo en el detalle del codigo).
# Los BRANDING/COPEAIRE no se restan aqui, sino al sumar subtotales/total.
res_st.setdefault((se, tp), {})[str(cod).strip()] = int(res_val or 0)
filas = []
@@ -558,10 +588,14 @@ def tabla_pauta(leads, matriculas, ano, mes, dia, importe_por_pauta_periodo=None
d = cods[cod]
imp_cod = float(imp_map.get(str(cod).strip(), 0.0))
res_cod = int(res_map.get(str(cod).strip(), 0))
# El detalle muestra el resultado REAL; pero si es branding NO suma
# al subtotal ni al total.
es_brand = str(cod).strip() in codigos_branding
codigos.append({"codigo": cod, **_fila(d), "importe": round(imp_cod, 2),
"resultados": res_cod})
t_rec += d["recibidos"]; t_proc += d["procesados"]
t_mats |= d["mats"]; t_inv += d["inv"]; t_imp += imp_cod; t_res += res_cod
t_mats |= d["mats"]; t_inv += d["inv"]; t_imp += imp_cod
t_res += (0 if es_brand else res_cod)
subfilas.append({
"tipo": tp, "recibidos": t_rec, "procesados": t_proc,
"matriculas": len(t_mats), "inversion": round(t_inv, 0),
@@ -582,7 +616,8 @@ def tabla_pauta(leads, matriculas, ano, mes, dia, importe_por_pauta_periodo=None
t_imp = round(sum(float(v or 0.0) for cod, v in importe_por_pauta_periodo.items()
if str(cod).strip() in campanias_sede_cargo), 2)
t_res = sum(int(v or 0) for cod, v in resultados_por_pauta_periodo.items()
if str(cod).strip() in campanias_sede_cargo)
if str(cod).strip() in campanias_sede_cargo
and str(cod).strip() not in codigos_branding)
# matrículas total: unión global
all_mats = set()
for tel, e in mat_por_tel.items():
@@ -660,7 +695,8 @@ def matriz_cursos(cursos, leads, ano, mes, dia, pauta_map=None,
campanias_map=None, cartera_all=None, cartera_asig=None,
mat_por_indice=None, cartera_tels=None, cartera_origen=None,
importe_por_pauta=None, importe_por_pauta_periodo=None,
cartera_canal=None, conjunto_por_pauta=None, pautas_por_indice=None):
cartera_canal=None, conjunto_por_pauta=None, pautas_por_indice=None,
resultados_por_pauta=None):
pauta_map = pauta_map or {}
pautas_por_indice = pautas_por_indice or {} # {num_indice: [pautas]} (muchos-a-muchos)
campanias_map = campanias_map or {}
@@ -671,6 +707,7 @@ def matriz_cursos(cursos, leads, ano, mes, dia, pauta_map=None,
cartera_origen = cartera_origen or {}
importe_por_pauta = importe_por_pauta or {}
importe_por_pauta_periodo = importe_por_pauta_periodo or {}
resultados_por_pauta = resultados_por_pauta or {}
conjunto_por_pauta = conjunto_por_pauta or {} # {pauta: [conjuntos de anuncios]}
# cartera_canal = {"PAUTA": {"all","asig","tels"}, "OTROS": {...}} para el desglose por canal
cartera_canal = cartera_canal or {}
@@ -734,6 +771,9 @@ def matriz_cursos(cursos, leads, ano, mes, dia, pauta_map=None,
pre.append({
"num_indice": ni,
"personalizado": nombre,
# Nombre real del curso (para enlazar al apartado PROGRAMA, cuyo filtro
# usa dsc_det_programa). Solo se agrega; no cambia nada mas.
"dsc_det_programa": str(c.get("dsc_det_programa") or c.get("dsc_programa") or "").strip(),
"fch": fch,
"pauta": cods[0] if cods else None, # pauta principal (para mostrar)
"pautas": cods, # todas las pautas (para sumar)
@@ -816,6 +856,8 @@ def matriz_cursos(cursos, leads, ano, mes, dia, pauta_map=None,
importe_pauta = round(sum(float(importe_por_pauta.get(str(cd), 0.0)) for cd in cods), 2)
# IMPORTE PAUTA EN EL MES: igual, pero solo del periodo filtrado.
importe_pauta_mes = round(sum(float(importe_por_pauta_periodo.get(str(cd), 0.0)) for cd in cods), 2)
# LEADS META: Resultados TOTALES de la pauta (sin filtro de fecha).
resultados_pauta = int(sum(float(resultados_por_pauta.get(str(cd), 0.0)) for cd in cods))
# ── SUBFILAS por CANAL (PAUTA / OTROS) ── mismo criterio que la fila padre
# (SEDE+PROGRAMA vía campanias, misma fecha) + filtro de canal desde cartera.
@@ -870,10 +912,12 @@ def matriz_cursos(cursos, leads, ano, mes, dia, pauta_map=None,
filas.append({
"num_indice": r["num_indice"],
"personalizado": r["personalizado"],
"dsc_det_programa": r.get("dsc_det_programa", ""), # para enlazar al apartado PROGRAMA
"fecha_inicio": r["fecha_inicio"],
"pauta": r.get("pauta"),
"importe_pauta": importe_pauta,
"importe_pauta_mes": importe_pauta_mes,
"resultados_pauta": resultados_pauta,
"cartera_total": cartera_total,
"cartera_total_asig": cartera_total_asig,
"matriculas_no_iden": matriculas_no_iden,