From 957b9e5cb330bdb2c917fb2c6ec08ec66f8d264a Mon Sep 17 00:00:00 2001 From: Administrator Date: Mon, 5 Oct 2026 17:13:45 -0500 Subject: [PATCH] Actualizar backend/modules/ventas/logic.py --- backend/modules/ventas/logic.py | 120 ++++++++++++++++++++++++++------ 1 file changed, 98 insertions(+), 22 deletions(-) diff --git a/backend/modules/ventas/logic.py b/backend/modules/ventas/logic.py index 749f6d0..e23e0fb 100644 --- a/backend/modules/ventas/logic.py +++ b/backend/modules/ventas/logic.py @@ -25,10 +25,13 @@ class VentasLogic: return s return "LIMA" - def buscar_todos_matriculados(self): + def buscar_todos_matriculados(self, tc_por_mes=None): """Devuelve TODOS los matriculados (todos los años/meses) para el buscador. Ejecuta la query de matrículas quitando el filtro de YEAR/MONTH de fch_matricula. - Retorna filas con el mismo formato que el detalle (para reusar columnas).""" + Retorna filas con el mismo formato que el detalle (para reusar columnas). + tc_por_mes: dict {(ano,mes): imp_tc_del_mes} para convertir dólares según el + mes de la fecha de cancelación (si no hay TC del mes, usa el imp_tc del SQL).""" + tc_por_mes = tc_por_mes or {} import re as _re sql_base = self.data_manager.query_matriculas_sql if not sql_base: @@ -82,7 +85,7 @@ class VentasLogic: # CLASIFICAR sede: prioriza dsc_programa prog_clasificacion = (d.get('dsc_programa') or d.get('dsc_det_programa') or d.get('dsc_promocion') or '') vendedor = d.get('dsc_vendedor', 'SIN VENDEDOR') or 'SIN VENDEDOR' - inv = d.get('INV_NETA', 0) or 0 + inv = float(d.get('INV_NETA', 0) or 0) saldo_mat = d.get('imp_saldo_matricula', 0) or 0 saldo_c1 = d.get('imp_saldo_cuota1', 0) or 0 def _fdate(v): @@ -93,7 +96,34 @@ class VentasLogic: return s f_mat = _fdate(d.get('fch_matricula')) f_ini = _fdate(d.get('fch_inicio')) - f_canc = _fdate(fechas_canc.get(mat_id)) + _canc_raw = fechas_canc.get(mat_id) + f_canc = _fdate(_canc_raw) + + # Conversión a soles si la INV_NETA está en DÓLARES. + # TC según el MES/AÑO de la fecha de cancelación: si hay "IMP_TC del mes" + # configurado para ese mes, se usa; si no, se usa el imp_tc del SQL. + cod_moneda = str(d.get('cod_moneda', 'SOL') or 'SOL').strip().upper() + if cod_moneda == 'DOL': + tc = 0.0 + # año/mes de la fecha de cancelación + _ym = None + if _canc_raw: + try: + if hasattr(_canc_raw, 'year'): + _ym = (int(_canc_raw.year), int(_canc_raw.month)) + else: + _s = str(_canc_raw)[:10] + _p = _s.split('-') + if len(_p) == 3: _ym = (int(_p[0]), int(_p[1])) + except Exception: + _ym = None + if _ym and _ym in tc_por_mes: + tc = float(tc_por_mes[_ym]) # IMP_TC del mes de la cancelación + if tc <= 0: + try: tc = float(d.get('imp_tc', 0) or 0) # fallback: TC del SQL + except: tc = 0.0 + if tc and tc > 0: + inv = round(inv * tc, 2) sede = self._clasificar_sede_comisiones(prog_clasificacion) # mismo layout que el detalle: 16 columnas fila = [ @@ -164,8 +194,13 @@ class VentasLogic: lista_vip_str.append(str(val)) except: pass - # Lista PURA de BASE_PENDIENTES (sin overrides). Venta Pendientes usa SOLO esto. + # BASE_PENDIENTES: lista base + matrículas con override de fecha (editadas). + # Si editaste la fecha de cancelación de un alumno, también puede contar en + # "Venta Pendientes" (el filtro real de si cuenta lo hace la fecha del override + # más abajo; los vaciados NO cuentan por fecha_vaciada_override). set_lista_pendientes = set(set_vip_actual) + set_base_pura = set(set_vip_actual) # lista BASE sin overrides (para la regla) + set_override_fecha = set() # matrículas que entran por override # Incluir matrículas con override de fecha (igual que ejecutar_consulta_ventas) _fov_vip = getattr(self.data_manager, '_fecha_canc_overrides', None) or {} @@ -173,6 +208,8 @@ class VentasLogic: try: val = int(float(str(mk))) set_vip_actual.add(val) + set_lista_pendientes.add(val) # las editadas también pueden ser pendientes + set_override_fecha.add(val) if str(val) not in lista_vip_str: lista_vip_str.append(str(val)) except: pass @@ -224,15 +261,26 @@ class VentasLogic: # (una con fch_cancelacion y otra sin). Nos quedamos con UNA sola fila por # matrícula, priorizando la que TENGA fecha de cancelación (verdad del pago). _dedup = {} + _max_inv = {} # mat -> mayor INV_NETA visto (por si la fila elegida trae 0) for _d in datos_raw: _m = str(_d.get('num_matricula', '')).strip() if _m.endswith('.0'): _m = _m[:-2] + try: _inv_d = float(_d.get('INV_NETA', 0) or 0) + except: _inv_d = 0.0 + if _inv_d > _max_inv.get(_m, 0.0): _max_inv[_m] = _inv_d _prev = _dedup.get(_m) _tiene_f = bool(_d.get('fch_cancelacion_cuota1')) if _prev is None: _dedup[_m] = _d elif _tiene_f and not bool(_prev.get('fch_cancelacion_cuota1')): _dedup[_m] = _d # reemplazar por la que sí tiene fecha + # Si la fila elegida quedó con INV_NETA 0 pero otra fila de la misma matrícula + # traía el monto, recuperarlo (el LEFT JOIN puede partir fecha y monto). + for _m, _d in _dedup.items(): + try: _inv_cur = float(_d.get('INV_NETA', 0) or 0) + except: _inv_cur = 0.0 + if _inv_cur <= 0 and _max_inv.get(_m, 0.0) > 0: + _d['INV_NETA'] = _max_inv[_m] datos_raw = list(_dedup.values()) # 🔥 DESCARGA DEL CRONOGRAMA COMPLETO (SIN IMPORTAR EL MES) 🔥 @@ -386,6 +434,16 @@ class VentasLogic: if cod_moneda == 'DOL': inv_neta_soles = round(inv_neta_raw * imp_tc, 2) else: inv_neta_soles = round(inv_neta_raw, 2) + # Override de INVERSIÓN NETA (Supabase): monto final en soles, reemplaza + # al SQL. Igual que en data_manager, para que editados no salgan en S/ 0. + _inv_ov = getattr(self.data_manager, '_inv_neta_overrides', None) + if _inv_ov: + _mk_inv = str(mat_id).strip() + if _mk_inv.endswith('.0'): _mk_inv = _mk_inv[:-2] + if _mk_inv in _inv_ov: + try: inv_neta_soles = round(float(_inv_ov[_mk_inv]), 2) + except: pass + row_year, row_month = -1, -1 fecha_mat_limpia = "" if fch_mat: @@ -410,6 +468,8 @@ class VentasLogic: except: fch_canc_limpia = str(fch_canc_raw)[:10] clase_alumno = self.clasificar_programa(nombre_prog_crudo) + # Sede del alumno (LIMA/AREQUIPA/PIURA/TRUJILLO) para la regla de PROMEDIO CUOTA. + _sede_prom = self._clasificar_sede_comisiones(nombre_prog_crudo) linea_actual = self.identificar_linea_carrera(nombre_prog_crudo) fecha_inicio_actual = fecha_inicio_prog.get(nombre_prog_crudo) @@ -484,18 +544,13 @@ class VentasLogic: # =============================================================== # DIVISIÓN SEGÚN CLASIFICACIÓN # =============================================================== - if suma_cuotas > 0: - if clase_alumno in ["TEAC", "TERC", "CARRERA"]: - valor_cuota_final = suma_cuotas / 5.0 - elif clase_alumno in ["AREQUIPA", "TRUJILLO", "PIURA"]: - valor_cuota_final = suma_cuotas / 6.0 - else: - valor_cuota_final = suma_cuotas - + # PROMEDIO CUOTA: SOLO para LIMA TEAC o LIMA TERC → suma cuotas / 5. + # Cualquier otro (seminarios, otras sedes, CARRERA/OTROS) → VACÍO. + if suma_cuotas > 0 and _sede_prom == "LIMA" and clase_alumno in ["TEAC", "TERC"]: + valor_cuota_final = suma_cuotas / 5.0 str_cuota_val = f"S/ {valor_cuota_final:,.0f}" else: valor_cuota_final = 0.0 - # Si no tiene cuotas mayores a 0, se queda en blanco limpio str_cuota_val = "" # =============================================================== @@ -583,7 +638,9 @@ class VentasLogic: # Índice [15]: num_matricula (clave para overrides de Comisiones) mat_id, # Índice [16]: nombre para CLASIFICAR (prioriza dsc_programa). Oculto en UI. - prog_clasificacion + prog_clasificacion, + # Índice [17]: moneda (SOL/DOL) — para el export de Comisiones. + cod_moneda, ] es_venta_del_mes = (str(row_year) == str(ano)) and (str(row_month) == str(mes_numero)) @@ -638,18 +695,37 @@ class VentasLogic: # VENTA PENDIENTES: SOLO de tu lista BASE_PENDIENTES y que pagaron su 1ª # cuota EN EL MES/AÑO DEL FILTRO. Usa la FECHA REAL DEL SQL (sin override), # porque los overrides NO deben afectar Venta Pendientes. + # El OVERRIDE manda: usa la fecha con override aplicado (fch_canc_raw). + # Si el override la vació, NO cuenta (aunque el SQL tenga fecha). + # Parseo tolerante: acepta datetime, 'yyyy-mm-dd' o 'dd/mm/yyyy'. _pago_mes_real = False - if fch_canc_real_sql and str(fch_canc_real_sql).strip() != "None": + if not fecha_vaciada_override and fch_canc_raw and str(fch_canc_raw).strip() not in ("", "None"): + _fa, _fm = None, None try: - if isinstance(fch_canc_real_sql, str): - _fo = datetime.strptime(fch_canc_real_sql[:10], '%Y-%m-%d') - _fa, _fm = _fo.year, _fo.month + if hasattr(fch_canc_raw, "year"): + _fa, _fm = fch_canc_raw.year, fch_canc_raw.month else: - _fa, _fm = fch_canc_real_sql.year, fch_canc_real_sql.month - if int(_fa) == int(ano) and int(_fm) == int(mes_numero): + _s = str(fch_canc_raw).strip()[:10] + if len(_s) == 10 and _s[4] == '-': # yyyy-mm-dd + _p = _s.split('-'); _fa, _fm = int(_p[0]), int(_p[1]) + elif len(_s) == 10 and _s[2] == '/': # dd/mm/yyyy + _p = _s.split('/'); _fa, _fm = int(_p[2]), int(_p[1]) + if _fa is not None and int(_fa) == int(ano) and int(_fm) == int(mes_numero): _pago_mes_real = True + except Exception: + pass + # Las que entran SOLO por override de fecha (no están en la lista base) + # deben ser de un mes ANTERIOR al filtro: Venta Pendientes = "mes pasado". + _es_pend = matricula_int in set_lista_pendientes + if _es_pend and matricula_int in set_override_fecha and matricula_int not in set_base_pura: + _mat_anterior = False + try: + _ym_mat = int(row_year) * 100 + int(row_month) + _ym_filtro = int(ano) * 100 + int(mes_numero) + _mat_anterior = _ym_mat < _ym_filtro except: pass - if matricula_int in set_lista_pendientes and _pago_mes_real: + _es_pend = _mat_anterior + if _es_pend and _pago_mes_real: agregar_a_lista("Venta Pendientes")