Solucionado submodulos y subiendo codigo real
This commit is contained in:
0
backend/modules/saldo_pendiente/__init__.py
Normal file
0
backend/modules/saldo_pendiente/__init__.py
Normal file
193
backend/modules/saldo_pendiente/logic.py
Normal file
193
backend/modules/saldo_pendiente/logic.py
Normal file
@@ -0,0 +1,193 @@
|
||||
# modules/saldo_pendiente/logic.py
|
||||
from .processor import SaldoProcessor
|
||||
from datetime import datetime
|
||||
|
||||
class SaldoLogic:
|
||||
"""
|
||||
Controlador lógico optimizado.
|
||||
1. Filtro Global Manual (lista_saldo_pendiente).
|
||||
2. Filtro REFRIPERU (Precio Neto < 1900/1000 o Etiquetas).
|
||||
3. Filtros de Negocio (Vendedor, Fecha, Estado).
|
||||
"""
|
||||
|
||||
def __init__(self, data_manager):
|
||||
self.data_manager = data_manager
|
||||
self.processor = SaldoProcessor(self.data_manager)
|
||||
|
||||
def obtener_saldos_consolidados(self, tipo_cuota):
|
||||
# 1. Recargar Configuración (Para leer tu nueva lista en vivo)
|
||||
try: self.data_manager.cargar_toda_configuracion()
|
||||
except: pass
|
||||
|
||||
all_debtors = []
|
||||
years_to_scan = [2025, 2026]
|
||||
|
||||
print(f"⚡ [Logic] Iniciando carga para {tipo_cuota}...")
|
||||
|
||||
for year in years_to_scan:
|
||||
try:
|
||||
# Usamos la consulta anual optimizada
|
||||
raw_year_data = self.data_manager.ejecutar_consulta_saldos_anual(str(year))
|
||||
if raw_year_data:
|
||||
datos_procesados = self._procesar_lote_local(raw_year_data, tipo_cuota)
|
||||
all_debtors.extend(datos_procesados)
|
||||
except AttributeError:
|
||||
print("⚠️ Error: DataManager no actualizado.")
|
||||
continue
|
||||
|
||||
# Ordenar alfabéticamente
|
||||
all_debtors.sort(key=lambda x: (x.get('VENDEDOR', '') or "ZZZ", x.get('ALUMNO', '') or ""))
|
||||
print(f"✅ Carga Finalizada. Total en tabla: {len(all_debtors)}")
|
||||
return all_debtors
|
||||
|
||||
def _procesar_lote_local(self, raw_data, tipo_cuota):
|
||||
datos_limpios = []
|
||||
|
||||
# --- Configuración ---
|
||||
ahora = datetime.now()
|
||||
ano_actual = ahora.year
|
||||
mes_actual = ahora.month
|
||||
|
||||
config = self.data_manager.config_data
|
||||
|
||||
# 1. CARGAMOS TUS LISTAS DEL JSON
|
||||
correcciones_refri = config.get("correcciones_descuento", {})
|
||||
exclusion_manual_global = config.get("lista_saldo_pendiente", {}) # <--- TU NUEVA LISTA
|
||||
|
||||
# 2. Lista Vendedores Permitidos
|
||||
lista_raw = config.get("lista_pendientes", [])
|
||||
if not lista_raw:
|
||||
lista_raw = ["AGUILAR U. JUAN CARLOS", "CHAVEZ P. DIANA", "HUAMAN C. ALONSO AGUSTIN",
|
||||
"LA ROSA C. VERONICA ASTRID", "LAZARO Q. DIEGO ARTURO",
|
||||
"MONTOYA D. CARMEN ISABEL", "PERALTA C. ALMENDRA LUCIA"]
|
||||
lista_permitidos = set([str(v).strip().upper() for v in lista_raw])
|
||||
|
||||
# 3. Mapeo de columnas
|
||||
mapa_cols = {
|
||||
"1° Cuota": ("imp_saldo_cuota1", "fch_venc_cuota1"),
|
||||
"2° Cuota": ("imp_saldo_cuota2", "fch_venc_cuota2"),
|
||||
"3° Cuota": ("imp_saldo_cuota3", "fch_venc_cuota3"),
|
||||
"4° Cuota": ("imp_saldo_cuota4", "fch_venc_cuota4"),
|
||||
"5° Cuota": ("imp_saldo_cuota5", "fch_venc_cuota5"),
|
||||
}
|
||||
col_saldo, col_venc = mapa_cols.get(tipo_cuota, (None, None))
|
||||
if not col_saldo: return []
|
||||
|
||||
for row in raw_data:
|
||||
# Identificadores
|
||||
num_mat = str(row.get('num_matricula', '')).split('.')[0].strip()
|
||||
|
||||
# =================================================================
|
||||
# 🛑 FILTRO 0: EXCLUSIÓN GLOBAL MANUAL (TU NUEVO PEDIDO)
|
||||
# =================================================================
|
||||
# Si la matrícula está en "lista_saldo_pendiente" con valor "NO", ADIÓS.
|
||||
if num_mat in exclusion_manual_global:
|
||||
val_excl = str(exclusion_manual_global[num_mat]).strip().upper()
|
||||
if val_excl == "NO":
|
||||
continue # Se salta inmediatamente, no importa nada más.
|
||||
|
||||
# =================================================================
|
||||
# 🛑 FILTRO 1: DETECCIÓN DE REFRIPERU (PRECIO NETO / ETIQUETAS)
|
||||
# =================================================================
|
||||
es_refriperu = False
|
||||
|
||||
# A) Manual (correcciones_descuento)
|
||||
if num_mat in correcciones_refri:
|
||||
val = str(correcciones_refri[num_mat]).strip().upper()
|
||||
if val == "SI": es_refriperu = True
|
||||
elif val == "NO": es_refriperu = False # Forzamos a cobrar
|
||||
else:
|
||||
# B) Automático (Etiquetas)
|
||||
dsc_beca = str(row.get('dsc_beca', '')).strip().upper()
|
||||
dsc_prog = str(row.get('dsc_programa', '')).strip().upper()
|
||||
dsc_prom = str(row.get('dsc_promocion', '')).strip().upper()
|
||||
|
||||
if 'REFRIPERU' in dsc_beca or '100%' in dsc_beca or 'BECA' in dsc_beca:
|
||||
es_refriperu = True
|
||||
elif 'REFRIPERU' in dsc_prog or 'REFRIPERU' in dsc_prom:
|
||||
es_refriperu = True
|
||||
|
||||
# C) Automático por PRECIO FINAL (INV_NETA)
|
||||
if not es_refriperu:
|
||||
try:
|
||||
# Usamos INV_NETA (Precio Real)
|
||||
inv_neta = float(row.get('INV_NETA', 0))
|
||||
except:
|
||||
inv_neta = 0.0
|
||||
|
||||
if inv_neta > 0:
|
||||
# Rango Técnicos/Especialistas
|
||||
if ("TECNICO" in dsc_prog or "ESPECIALISTA" in dsc_prog or "TEAC" in dsc_prog):
|
||||
if inv_neta < 1900: es_refriperu = True # ej: 1799
|
||||
|
||||
# Rango Gestión/Ventas
|
||||
elif "GESTION" in dsc_prog or "VENTA" in dsc_prog:
|
||||
if inv_neta < 1000: es_refriperu = True
|
||||
|
||||
if es_refriperu:
|
||||
continue
|
||||
|
||||
# =================================================================
|
||||
# 🛑 FILTROS ESTÁNDAR (VENDEDOR, ESTADO, FECHA)
|
||||
# =================================================================
|
||||
|
||||
# Vendedor
|
||||
vendedor = str(row.get('dsc_vendedor', '')).strip().upper()
|
||||
if lista_permitidos:
|
||||
encontrado = False
|
||||
if vendedor in lista_permitidos: encontrado = True
|
||||
else:
|
||||
for p in lista_permitidos:
|
||||
if p.replace(" ","") in vendedor.replace(" ",""):
|
||||
encontrado = True; break
|
||||
if not encontrado: continue
|
||||
|
||||
# Estado
|
||||
estado = str(row.get('estado_matricula', '')).strip().upper()
|
||||
if estado not in ('ALU', 'PRE'): continue
|
||||
|
||||
# Fecha Futura (Solo 2da cuota en adelante)
|
||||
if tipo_cuota != "1° Cuota":
|
||||
fch_raw = row.get(col_venc)
|
||||
if fch_raw:
|
||||
try:
|
||||
if isinstance(fch_raw, str): f_obj = datetime.strptime(fch_raw[:10], '%Y-%m-%d')
|
||||
else: f_obj = fch_raw
|
||||
if f_obj.year > ano_actual: continue
|
||||
elif f_obj.year == ano_actual and f_obj.month > mes_actual: continue
|
||||
except: pass
|
||||
|
||||
# Saldo Positivo
|
||||
try: val = float(row.get(col_saldo, 0))
|
||||
except: val = 0.0
|
||||
if val <= 0.1: continue
|
||||
|
||||
# --- 4. Construcción de Fila ---
|
||||
def fmt(v):
|
||||
if not v: return ""
|
||||
if isinstance(v, str): return v[:10]
|
||||
if isinstance(v, datetime): return v.strftime('%d-%m-%Y')
|
||||
return str(v)
|
||||
|
||||
fila = {
|
||||
'MATRICULA': row.get('num_matricula'),
|
||||
'VENDEDOR': row.get('dsc_vendedor'),
|
||||
'ALUMNO': row.get('dsc_alumno'),
|
||||
'PROGRAMA': row.get('dsc_promocion'),
|
||||
'FECHA INICIO': fmt(row.get('fch_inicio')),
|
||||
'FECHA MATR.': fmt(row.get('fch_matricula')),
|
||||
'SALDO MAT.': float(row.get('imp_saldo_matricula', 0)),
|
||||
f'SALDO {tipo_cuota.upper()}': val,
|
||||
'VENCIMIENTO': fmt(row.get(col_venc)),
|
||||
'INV. NETA': float(row.get('INV_NETA', 0)),
|
||||
'DNI': row.get('dsc_documento', ''),
|
||||
'CELULAR': row.get('dsc_telefono_1', '')
|
||||
}
|
||||
datos_limpios.append(fila)
|
||||
|
||||
return datos_limpios
|
||||
|
||||
# Métodos legacy
|
||||
def obtener_saldos(self, ano, mes, tipo_cuota): return self.processor.obtener_datos_procesados(ano, mes, tipo_cuota)
|
||||
def get_current_year(self): return self.data_manager.get_current_year()
|
||||
def get_current_month(self): return self.data_manager.get_current_month()
|
||||
162
backend/modules/saldo_pendiente/processor.py
Normal file
162
backend/modules/saldo_pendiente/processor.py
Normal file
@@ -0,0 +1,162 @@
|
||||
# modules/saldo_pendiente/processor.py
|
||||
from datetime import datetime
|
||||
|
||||
class SaldoProcessor:
|
||||
"""
|
||||
Procesador de lógica de negocio para Saldos Pendientes.
|
||||
Filtra estrictamente por estado ALU/PRE, saldos positivos,
|
||||
vendedores permitidos y FECHA DE VENCIMIENTO (No mostrar futuro).
|
||||
"""
|
||||
|
||||
def __init__(self, data_manager):
|
||||
self.data_manager = data_manager
|
||||
|
||||
def obtener_datos_procesados(self, ano, mes, tipo_cuota):
|
||||
"""
|
||||
Recupera datos crudos y aplica los filtros de negocio.
|
||||
"""
|
||||
# 1. Traer la data cruda desde DataManager
|
||||
raw_data = self.data_manager.ejecutar_consulta_saldos_pendientes(ano, mes)
|
||||
|
||||
datos_limpios = []
|
||||
|
||||
# 2. Obtener fecha actual para saber qué es "futuro"
|
||||
ahora = datetime.now()
|
||||
ano_actual = ahora.year
|
||||
mes_actual = ahora.month
|
||||
|
||||
# 3. Cargar la "Lista Blanca" de vendedores desde el JSON
|
||||
config = self.data_manager.config_data
|
||||
lista_raw = config.get("lista_pendientes", [])
|
||||
|
||||
# RESPALDO DE EMERGENCIA
|
||||
if not lista_raw:
|
||||
lista_raw = [
|
||||
"AGUILAR U. JUAN CARLOS",
|
||||
"CHAVEZ P. DIANA",
|
||||
"HUAMAN C. ALONSO AGUSTIN",
|
||||
"LA ROSA C. VERONICA ASTRID",
|
||||
"LAZARO Q. DIEGO ARTURO",
|
||||
"MONTOYA D. CARMEN ISABEL",
|
||||
"PERALTA C. ALMENDRA LUCIA"
|
||||
]
|
||||
|
||||
# Normalizamos la lista
|
||||
lista_permitidos = set([str(v).strip().upper() for v in lista_raw])
|
||||
|
||||
# 4. Mapeo de columnas según la selección
|
||||
mapa_columnas = {
|
||||
"1° Cuota": ("imp_saldo_cuota1", "fch_venc_cuota1"),
|
||||
"2° Cuota": ("imp_saldo_cuota2", "fch_venc_cuota2"),
|
||||
"3° Cuota": ("imp_saldo_cuota3", "fch_venc_cuota3"),
|
||||
}
|
||||
|
||||
col_saldo_target, col_venc_target = mapa_columnas.get(tipo_cuota, (None, None))
|
||||
|
||||
if not col_saldo_target:
|
||||
return []
|
||||
|
||||
for row in raw_data:
|
||||
# =================================================================
|
||||
# 🛑 FILTRO 1: VENDEDOR PERMITIDO
|
||||
# =================================================================
|
||||
vendedor_actual = str(row.get('dsc_vendedor', '')).strip().upper()
|
||||
|
||||
if lista_permitidos:
|
||||
if vendedor_actual not in lista_permitidos:
|
||||
# Búsqueda parcial por si hay errores de espacios
|
||||
encontrado = False
|
||||
for permitido in lista_permitidos:
|
||||
v_norm = vendedor_actual.replace(" ", "")
|
||||
p_norm = permitido.replace(" ", "")
|
||||
if p_norm in v_norm:
|
||||
encontrado = True
|
||||
break
|
||||
if not encontrado:
|
||||
continue
|
||||
|
||||
# =================================================================
|
||||
# 🛑 FILTRO 2: ESTADO (ALU/PRE)
|
||||
# =================================================================
|
||||
estado = str(row.get('estado_matricula', '')).strip().upper()
|
||||
if estado not in ('ALU', 'PRE'):
|
||||
continue
|
||||
|
||||
# =================================================================
|
||||
# 🛑 FILTRO 3: FECHA DE VENCIMIENTO (NO MOSTRAR FUTURO)
|
||||
# =================================================================
|
||||
# Este filtro aplica PRINCIPALMENTE para 2° Cuota en adelante.
|
||||
# (Aunque la lógica es válida para todas, la 1° suele ser inmediata).
|
||||
|
||||
if tipo_cuota != "1° Cuota":
|
||||
fch_venc_raw = row.get(col_venc_target)
|
||||
|
||||
es_futuro = False
|
||||
if fch_venc_raw:
|
||||
try:
|
||||
# Convertir a objeto fecha si es texto
|
||||
if isinstance(fch_venc_raw, str):
|
||||
# Asumimos formato SQL YYYY-MM-DD
|
||||
f_obj = datetime.strptime(fch_venc_raw[:10], '%Y-%m-%d')
|
||||
else:
|
||||
f_obj = fch_venc_raw # Ya es datetime
|
||||
|
||||
venc_ano = f_obj.year
|
||||
venc_mes = f_obj.month
|
||||
|
||||
# LÓGICA DE TIEMPO:
|
||||
# Si el año de vencimiento es mayor al actual -> ES FUTURO
|
||||
if venc_ano > ano_actual:
|
||||
es_futuro = True
|
||||
# Si es el mismo año, pero el mes es mayor al actual -> ES FUTURO
|
||||
elif venc_ano == ano_actual and venc_mes > mes_actual:
|
||||
es_futuro = True
|
||||
|
||||
except Exception as e:
|
||||
# Si falla la fecha, asumimos que no es futuro para no ocultar por error
|
||||
pass
|
||||
|
||||
# Si la cuota vence en el futuro (ej: Marzo cuando estamos en Febrero), LA OCULTAMOS.
|
||||
if es_futuro:
|
||||
continue
|
||||
|
||||
# =================================================================
|
||||
# 🛑 FILTRO 4: SALDO > 0
|
||||
# =================================================================
|
||||
try:
|
||||
val = row.get(col_saldo_target, 0)
|
||||
saldo_a_mostrar = float(val) if val is not None else 0.0
|
||||
except:
|
||||
saldo_a_mostrar = 0.0
|
||||
|
||||
if saldo_a_mostrar <= 0.1:
|
||||
continue
|
||||
|
||||
# --- FORMATEO PARA VISUALIZACIÓN ---
|
||||
def format_date(val):
|
||||
if not val: return ""
|
||||
if isinstance(val, str): return val[:10]
|
||||
if isinstance(val, datetime): return val.strftime('%d-%m-%Y')
|
||||
return str(val)
|
||||
|
||||
f_inicio = format_date(row.get('fch_inicio'))
|
||||
f_matr = format_date(row.get('fch_matricula'))
|
||||
f_vencimiento = format_date(row.get(col_venc_target))
|
||||
|
||||
# --- CONSTRUCCIÓN DE LA FILA FINAL ---
|
||||
fila = {
|
||||
'MATRICULA': row.get('num_matricula'),
|
||||
'VENDEDOR': row.get('dsc_vendedor'),
|
||||
'ALUMNO': row.get('dsc_alumno'),
|
||||
'PROGRAMA': row.get('dsc_promocion'),
|
||||
'FECHA INICIO': f_inicio,
|
||||
'FECHA MATR.': f_matr,
|
||||
'SALDO MAT.': float(row.get('imp_saldo_matricula', 0)),
|
||||
f'SALDO {tipo_cuota.upper()}': saldo_a_mostrar,
|
||||
'VENCIMIENTO': f_vencimiento, # Aquí se verá la fecha (ej: 14-02-2026)
|
||||
'INV. NETA': float(row.get('INV_NETA', 0)),
|
||||
}
|
||||
|
||||
datos_limpios.append(fila)
|
||||
|
||||
return datos_limpios
|
||||
Reference in New Issue
Block a user