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 "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")