From 86800569e9a1b273d274f2842912551957b0873d Mon Sep 17 00:00:00 2001 From: Administrator Date: Mon, 5 Oct 2026 13:36:41 -0500 Subject: [PATCH] Actualizar backend/modules/cobranza/processor.py --- backend/modules/cobranza/processor.py | 69 +++++++++++++++++++-------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/backend/modules/cobranza/processor.py b/backend/modules/cobranza/processor.py index 51e282d..ff5ce4a 100644 --- a/backend/modules/cobranza/processor.py +++ b/backend/modules/cobranza/processor.py @@ -388,7 +388,7 @@ class CobranzaProcessor: def obtener_datos_procesados(self, ano, mes, sectorista_filtro="TODOS", agrupacion="SEDE", sede_filtro="TODOS", prog_filtro="TODOS", venc_hasta=None, - mats_retirar=None): + mats_retirar=None, frec_filtro="TODOS"): fecha_filtro, fecha_filtro_siguiente = self.obtener_fechas_filtro(ano, mes) hoy = datetime.now() # Filtro adicional por F. Vencimiento: solo cuotas con vencimiento <= venc_hasta. @@ -414,6 +414,8 @@ class CobranzaProcessor: indices_cronograma = set() mapa_tc = {} mapa_programa = {} + mapa_clasif = {} # mat -> (sede_meta, filtro_prog) para filtrar por alumno + mapa_frec = {} # mat -> frecuencia (cod_frecuencia) para filtrar por alumno mapa_sectorista = {} # <-- NUEVO MAPA PARA CORREGIR ASESORES # Sectorista puede venir con varios separados por pipe (multi). Vacío/TODOS = todos. @@ -425,6 +427,9 @@ class CobranzaProcessor: _sede_all = (not _sede_set) or ("TODOS" in _sede_set) _prog_set = {p.strip().upper() for p in str(prog_filtro or "TODOS").split(",") if p.strip()} _prog_all = (not _prog_set) or ("TODOS" in _prog_set) + # Frecuencia multi (coma). Vacío/TODOS = todas. + _frec_set = {f.strip().upper() for f in str(frec_filtro or "TODOS").split(",") if f.strip()} + _frec_all = (not _frec_set) or ("TODOS" in _frec_set) for fila in cronograma_raw: sectorista_str = str(fila.get('dsc_sectorista', '')) @@ -451,21 +456,36 @@ class CobranzaProcessor: prog = str(fila.get('dsc_det_programa', '')).strip() mapa_programa[mat] = prog if (prog and prog != "None") else "SIN PROGRAMA" + # Clasificación SEDE/PROGRAMA por MATRICULA (1ª cuota), para filtrar por + # alumno completo igual que el detalle (Ver), no cuota por cuota. + # Usa dsc_det_programa (el MISMO campo que el detalle/Ver). + if mat not in mapa_clasif: + _pm = str(fila.get('dsc_det_programa', '')).strip() + if not _pm or _pm == "None": _pm = "SIN PROGRAMA" + mapa_clasif[mat] = ( + str(self.clasificar_sede_meta(_pm)).upper(), + str(self.clasificar_filtro_programa(_pm)).upper(), + ) + + # Frecuencia por MATRICULA (1ª cuota), para filtrar por alumno. + if mat not in mapa_frec: + _fm = str(fila.get('cod_frecuencia', '')).strip().upper() + mapa_frec[mat] = _fm if _fm and _fm != "NONE" else "" + for fila in cronograma_raw: idx = str(fila.get('num_indice', '')).strip() - if idx not in indices_cronograma: continue + if idx not in indices_cronograma: continue mat = str(fila.get('num_matricula', '')).strip() - # Filtro por SEDE y PROGRAMA (REPLACE_META) — se aplica en cualquier - # agrupación cuando se pide (usado sobre todo en ASESOR). - _prog = fila.get('dsc_programa', '') - if not _sede_all: - if str(self.clasificar_sede_meta(_prog)).upper() not in _sede_set: - continue - if not _prog_all: - if str(self.clasificar_filtro_programa(_prog)).upper() not in _prog_set: - continue + # Filtro por SEDE y PROGRAMA (REPLACE_META) — por MATRICULA (igual que "Ver"). + _cs, _cp = mapa_clasif.get(mat, ("", "")) + if not _sede_all and _cs not in _sede_set: + continue + if not _prog_all and _cp not in _prog_set: + continue + if not _frec_all and mapa_frec.get(mat, "") not in _frec_set: + continue # Filtro por F. Vencimiento (<=). Sin fecha => se incluye siempre. if venc_corte is not None: @@ -519,14 +539,14 @@ class CobranzaProcessor: mat_factura = str(fila.get('num_matricula', '')).strip() tc_correcto = mapa_tc.get(mat_factura, 1.0) - # Mismo filtro SEDE/PROGRAMA (REPLACE_META) que en el cronograma. - _progF = fila.get('dsc_programa', '') - if not _sede_all: - if str(self.clasificar_sede_meta(_progF)).upper() not in _sede_set: - continue - if not _prog_all: - if str(self.clasificar_filtro_programa(_progF)).upper() not in _prog_set: - continue + # Mismo filtro SEDE/PROGRAMA por MATRICULA (igual que el cronograma y "Ver"). + _csF, _cpF = mapa_clasif.get(mat_factura, ("", "")) + if not _sede_all and _csF not in _sede_set: + continue + if not _prog_all and _cpF not in _prog_set: + continue + if not _frec_all and mapa_frec.get(mat_factura, "") not in _frec_set: + continue # Filtro por F. Vencimiento (<=). En facturas la fecha es fch_vencimiento_cuota. # Sin fecha => se incluye siempre. @@ -666,11 +686,14 @@ class CobranzaProcessor: # ========================================================================= # DESGLOSE POR ALUMNO/MATRÍCULA # ========================================================================= - def obtener_detalle_programa(self, ano, mes, sectorista_filtro, programa_nombre, venc_hasta=None): + def obtener_detalle_programa(self, ano, mes, sectorista_filtro, programa_nombre, venc_hasta=None, frec_filtro="TODOS"): fecha_filtro, fecha_filtro_siguiente = self.obtener_fechas_filtro(ano, mes) hoy = datetime.now() # Filtro por F. Vencimiento (<=). Sin fecha => se incluye siempre. venc_corte = self.parse_corte_venc(venc_hasta) + # Frecuencia multi (coma). Vacío/TODOS = todas. + _frec_set = {f.strip().upper() for f in str(frec_filtro or "TODOS").split(",") if f.strip()} + _frec_all = (not _frec_set) or ("TODOS" in _frec_set) try: cronograma_raw, facturas_raw = self._obtener_datos_cached(ano) @@ -697,7 +720,11 @@ class CobranzaProcessor: if not prog or prog == "None": prog = "SIN PROGRAMA" if programa_nombre != "TODOS": - if self._aplicar_reemplazos_nombre(prog) != programa_nombre: continue + if self._aplicar_reemplazos_nombre(prog) != programa_nombre: continue + + if not _frec_all: + _fm = str(fila.get('cod_frecuencia', '')).strip().upper() + if (_fm if _fm and _fm != "NONE" else "") not in _frec_set: continue idx = str(fila.get('num_indice', '')).strip() if idx: indices_cronograma.add(idx)