diff --git a/backend/leads_logic.py b/backend/leads_logic.py index b3efe68..d15f7d2 100644 --- a/backend/leads_logic.py +++ b/backend/leads_logic.py @@ -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,