Actualizar backend/modules/ventas/logic.py

This commit is contained in:
2026-10-05 17:13:45 -05:00
parent 1d2bd45a70
commit 957b9e5cb3

View File

@@ -25,10 +25,13 @@ class VentasLogic:
return s return s
return "LIMA" 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. """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. 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 import re as _re
sql_base = self.data_manager.query_matriculas_sql sql_base = self.data_manager.query_matriculas_sql
if not sql_base: if not sql_base:
@@ -82,7 +85,7 @@ class VentasLogic:
# CLASIFICAR sede: prioriza dsc_programa # CLASIFICAR sede: prioriza dsc_programa
prog_clasificacion = (d.get('dsc_programa') or d.get('dsc_det_programa') or d.get('dsc_promocion') or '') 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' 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_mat = d.get('imp_saldo_matricula', 0) or 0
saldo_c1 = d.get('imp_saldo_cuota1', 0) or 0 saldo_c1 = d.get('imp_saldo_cuota1', 0) or 0
def _fdate(v): def _fdate(v):
@@ -93,7 +96,34 @@ class VentasLogic:
return s return s
f_mat = _fdate(d.get('fch_matricula')) f_mat = _fdate(d.get('fch_matricula'))
f_ini = _fdate(d.get('fch_inicio')) 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) sede = self._clasificar_sede_comisiones(prog_clasificacion)
# mismo layout que el detalle: 16 columnas # mismo layout que el detalle: 16 columnas
fila = [ fila = [
@@ -164,8 +194,13 @@ class VentasLogic:
lista_vip_str.append(str(val)) lista_vip_str.append(str(val))
except: pass 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_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) # Incluir matrículas con override de fecha (igual que ejecutar_consulta_ventas)
_fov_vip = getattr(self.data_manager, '_fecha_canc_overrides', None) or {} _fov_vip = getattr(self.data_manager, '_fecha_canc_overrides', None) or {}
@@ -173,6 +208,8 @@ class VentasLogic:
try: try:
val = int(float(str(mk))) val = int(float(str(mk)))
set_vip_actual.add(val) 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: if str(val) not in lista_vip_str:
lista_vip_str.append(str(val)) lista_vip_str.append(str(val))
except: pass except: pass
@@ -224,15 +261,26 @@ class VentasLogic:
# (una con fch_cancelacion y otra sin). Nos quedamos con UNA sola fila por # (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). # matrícula, priorizando la que TENGA fecha de cancelación (verdad del pago).
_dedup = {} _dedup = {}
_max_inv = {} # mat -> mayor INV_NETA visto (por si la fila elegida trae 0)
for _d in datos_raw: for _d in datos_raw:
_m = str(_d.get('num_matricula', '')).strip() _m = str(_d.get('num_matricula', '')).strip()
if _m.endswith('.0'): _m = _m[:-2] 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) _prev = _dedup.get(_m)
_tiene_f = bool(_d.get('fch_cancelacion_cuota1')) _tiene_f = bool(_d.get('fch_cancelacion_cuota1'))
if _prev is None: if _prev is None:
_dedup[_m] = _d _dedup[_m] = _d
elif _tiene_f and not bool(_prev.get('fch_cancelacion_cuota1')): elif _tiene_f and not bool(_prev.get('fch_cancelacion_cuota1')):
_dedup[_m] = _d # reemplazar por la que sí tiene fecha _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()) datos_raw = list(_dedup.values())
# 🔥 DESCARGA DEL CRONOGRAMA COMPLETO (SIN IMPORTAR EL MES) 🔥 # 🔥 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) if cod_moneda == 'DOL': inv_neta_soles = round(inv_neta_raw * imp_tc, 2)
else: inv_neta_soles = round(inv_neta_raw, 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 row_year, row_month = -1, -1
fecha_mat_limpia = "" fecha_mat_limpia = ""
if fch_mat: if fch_mat:
@@ -410,6 +468,8 @@ class VentasLogic:
except: fch_canc_limpia = str(fch_canc_raw)[:10] except: fch_canc_limpia = str(fch_canc_raw)[:10]
clase_alumno = self.clasificar_programa(nombre_prog_crudo) 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) linea_actual = self.identificar_linea_carrera(nombre_prog_crudo)
fecha_inicio_actual = fecha_inicio_prog.get(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 # DIVISIÓN SEGÚN CLASIFICACIÓN
# =============================================================== # ===============================================================
if suma_cuotas > 0: # PROMEDIO CUOTA: SOLO para LIMA TEAC o LIMA TERC → suma cuotas / 5.
if clase_alumno in ["TEAC", "TERC", "CARRERA"]: # Cualquier otro (seminarios, otras sedes, CARRERA/OTROS) → VACÍO.
valor_cuota_final = suma_cuotas / 5.0 if suma_cuotas > 0 and _sede_prom == "LIMA" and clase_alumno in ["TEAC", "TERC"]:
elif clase_alumno in ["AREQUIPA", "TRUJILLO", "PIURA"]: valor_cuota_final = suma_cuotas / 5.0
valor_cuota_final = suma_cuotas / 6.0
else:
valor_cuota_final = suma_cuotas
str_cuota_val = f"S/ {valor_cuota_final:,.0f}" str_cuota_val = f"S/ {valor_cuota_final:,.0f}"
else: else:
valor_cuota_final = 0.0 valor_cuota_final = 0.0
# Si no tiene cuotas mayores a 0, se queda en blanco limpio
str_cuota_val = "" str_cuota_val = ""
# =============================================================== # ===============================================================
@@ -583,7 +638,9 @@ class VentasLogic:
# Índice [15]: num_matricula (clave para overrides de Comisiones) # Índice [15]: num_matricula (clave para overrides de Comisiones)
mat_id, mat_id,
# Índice [16]: nombre para CLASIFICAR (prioriza dsc_programa). Oculto en UI. # Í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)) 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ª # 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), # cuota EN EL MES/AÑO DEL FILTRO. Usa la FECHA REAL DEL SQL (sin override),
# porque los overrides NO deben afectar Venta Pendientes. # 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 _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: try:
if isinstance(fch_canc_real_sql, str): if hasattr(fch_canc_raw, "year"):
_fo = datetime.strptime(fch_canc_real_sql[:10], '%Y-%m-%d') _fa, _fm = fch_canc_raw.year, fch_canc_raw.month
_fa, _fm = _fo.year, _fo.month
else: else:
_fa, _fm = fch_canc_real_sql.year, fch_canc_real_sql.month _s = str(fch_canc_raw).strip()[:10]
if int(_fa) == int(ano) and int(_fm) == int(mes_numero): 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 _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 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") agregar_a_lista("Venta Pendientes")