Files
dashboard-completo/backend/modules/rentabilidad/processor.py
2026-06-27 11:58:05 -05:00

290 lines
14 KiB
Python

# modules/rentabilidad/processor.py
from datetime import datetime
import unicodedata
class RentabilidadProcessor:
"""Procesador de datos para Rentabilidad - Hereda la lógica estricta de Ocupabilidad"""
def __init__(self, data_manager):
self.data_manager = data_manager
# Columnas exclusivas de rentabilidad
self.columnas = [
'programa_frecuencia',
'fch_inicio',
'Descuento', # INSCRITOS REFRIPERU
'Inscritos_Continuidad', # INSCRITOS CONTINUIDAD
'Inscritos_Nuevos', # Calculado en Logic
'Inscritos_Totales', # TOTAL INSCRITOS
'Inscritos_PC', # INSCRITOS P.C
'Retirados', # INSCRITOS RETIRADOS
'Inscritos_En_Curso', # INSCRITOS ACTIVOS
'Promedio_Cuota', # Pendiente (0)
'Promedio_Desc_E', # Pendiente (0)
'Valor_Venta', # Pendiente (0)
'Opciones' # Fijo "en cu"
]
def obtener_datos_procesados(self, ano, mes):
try:
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=3) as executor:
fut_cursos = executor.submit(self.data_manager.ejecutar_consulta_cursos, ano, mes)
fut_raw = executor.submit(self.data_manager.ejecutar_consulta_matriculados_detalle, ano, mes)
fut_matriculas = executor.submit(self.data_manager.cargar_datos_matriculas, ano, mes)
datos_originales = fut_cursos.result()
datos_raw_matriculas = fut_raw.result()
datos_matriculas = fut_matriculas.result()
lista_dnis = []
if datos_raw_matriculas:
for alumno in datos_raw_matriculas:
dni = alumno.get('dsc_documento')
if dni: lista_dnis.append(dni)
datos_historial = self.data_manager.consultar_historial_continuidad(lista_dnis)
datos_procesados = self.aplicar_personalizaciones(
datos_originales, datos_matriculas, datos_raw_matriculas, datos_historial, ano, mes
)
return datos_procesados
except Exception as e:
print(f"❌ Error obteniendo datos rentabilidad: {e}")
return []
def normalizar_texto(self, texto):
if not texto: return ""
texto = str(texto).upper().strip()
texto = unicodedata.normalize('NFD', texto)
return texto.encode('ascii', 'ignore').decode("utf-8")
def obtener_categoria_programa(self, nombre_programa):
try:
nombre_limpio = self.normalizar_texto(nombre_programa)
clasificaciones = self.data_manager.meta_data.get('clasificacion_programas', {})
for categoria, config in clasificaciones.items():
patrones = config.get('patrones', [])
for patron in patrones:
if self.normalizar_texto(patron) in nombre_limpio:
return categoria
return "OTROS"
except: return "OTROS"
def aplicar_personalizaciones(self, datos_originales, datos_matriculas, datos_raw_matriculas, datos_historial, ano_filtro, mes_filtro):
print("💰 Procesando variables de Rentabilidad...")
cursos_personalizados = self.data_manager.config_data.get('cursos_personalizados', {})
datos_filtrados = []
try: ano_target, mes_target = int(ano_filtro), int(mes_filtro)
except: ano_target, mes_target = 0, 0
# FILTRADO Y ACTUALIZADOR EN VIVO
for curso in datos_originales:
num_indice = str(curso.get('num_indice', ''))
if num_indice in cursos_personalizados:
curso.update(cursos_personalizados[num_indice])
if curso.get('flg_activo', '') == 'NO': continue
# Si el curso está suspendido, lo ignoramos y no se muestra en Rentabilidad
if str(curso.get('cod_estado', '')).strip().upper() == 'SUS': continue
fch_valida = True
try:
raw_fecha = curso.get('fch_inicio')
if raw_fecha:
s_fecha = str(raw_fecha).strip()
if len(s_fecha) == 10 and s_fecha[2] == '-': f_obj = datetime.strptime(s_fecha, '%d-%m-%Y')
elif ' ' in s_fecha: f_obj = datetime.strptime(s_fecha.split('.')[0], '%Y-%m-%d %H:%M:%S')
else: f_obj = datetime.strptime(s_fecha, '%Y-%m-%d')
if f_obj and (f_obj.year != ano_target or f_obj.month != mes_target):
fch_valida = False
except: pass
if fch_valida: datos_filtrados.append(curso)
datos_filtrados = self.aplicar_reemplazos_programas(datos_filtrados)
datos_filtrados = self.concatenar_programa_frecuencia(datos_filtrados)
for c in datos_filtrados: c['fch_inicio'] = self.formatear_fecha(c.get('fch_inicio', ''))
# CÁLCULOS CENTRALES
datos_filtrados, alumnos_refriperu = self.agregar_descuento(datos_filtrados, datos_raw_matriculas)
datos_filtrados = self.agregar_continuidad(datos_filtrados, datos_raw_matriculas, datos_historial, alumnos_refriperu)
datos_filtrados = self.agregar_inscritos_mes(datos_filtrados, datos_matriculas)
datos_filtrados = self.agregar_inscritos_pc(datos_filtrados, datos_raw_matriculas)
# EXTRACCIÓN REAL DE BD Y VALORES PENDIENTES
for c in datos_filtrados:
c['Retirados'] = int(c.get('Inscritos_Retirados', 0)) # <--- AHORA JALA LA DATA REAL DEL SQL
c['Promedio_Cuota'] = 0.0
c['Promedio_Desc_E'] = 0.0
c['Valor_Venta'] = 0.0
c['Opciones'] = "en cu"
return datos_filtrados
def aplicar_reemplazos_programas(self, datos):
reemplazos = self.data_manager.replace_data.get('reemplazos_programas', {})
for curso in datos:
prog = curso.get('dsc_programa', '')
if prog in reemplazos: curso['dsc_programa'] = reemplazos[prog]
return datos
def concatenar_programa_frecuencia(self, datos):
for c in datos:
p, f = c.get('dsc_programa', ''), c.get('cod_frecuencia', '')
c['programa_frecuencia'] = f"{p} - {f}" if p and f else p or f or ""
return datos
def formatear_fecha(self, fecha_str):
try:
if not fecha_str: return ""
if len(str(fecha_str)) == 10 and str(fecha_str)[2] == '-': return str(fecha_str)
if '.' in str(fecha_str): return datetime.strptime(str(fecha_str), '%Y-%m-%d %H:%M:%S.%f').strftime('%d-%m-%Y')
return datetime.strptime(str(fecha_str), '%Y-%m-%d %H:%M:%S').strftime('%d-%m-%Y')
except: return str(fecha_str)
def identificar_linea_carrera(self, nombre_programa):
categoria = self.obtener_categoria_programa(nombre_programa)
if categoria in ["AREQUIPA", "TRUJILLO", "PIURA", "TEAC", "TERC"]:
return "CARRERA_COMPATIBLE"
return None
def parse_fecha(self, fecha_raw):
try:
if isinstance(fecha_raw, datetime): return fecha_raw
if not fecha_raw: return None
s_fecha = str(fecha_raw).strip()
if len(s_fecha) == 10 and s_fecha[2] == '-': return datetime.strptime(s_fecha, '%d-%m-%Y')
if ' ' in s_fecha: return datetime.strptime(s_fecha.split('.')[0], '%Y-%m-%d %H:%M:%S')
return datetime.strptime(s_fecha, '%Y-%m-%d')
except: return None
# Lógica estricta de Refriperu
def agregar_descuento(self, datos, datos_raw_matriculas):
conteo_por_curso = {}
alumnos_refriperu = set()
correcciones_descuento = self.data_manager.config_data.get('correcciones_descuento', {})
if datos_raw_matriculas:
for alumno in datos_raw_matriculas:
try:
if str(alumno.get('estado_matricula', '')).strip().upper() not in ['ALU', 'PRE']: continue
num_indice = str(alumno.get('num_indice', ''))
mat_id = str(alumno.get('num_matricula', '')).strip()
if mat_id.endswith('.0'): mat_id = mat_id[:-2]
if mat_id in correcciones_descuento:
if str(correcciones_descuento[mat_id]).upper().strip() == "SI":
conteo_por_curso[num_indice] = conteo_por_curso.get(num_indice, 0) + 1
alumnos_refriperu.add(mat_id)
continue
elif str(correcciones_descuento[mat_id]).upper().strip() == "NO": continue
nombre_programa = str(alumno.get('dsc_programa', ''))
cod_moneda = str(alumno.get('cod_moneda', 'SOL')).strip().upper()
try:
inv_neta = float(alumno.get('INV_NETA', 0) or 0)
imp_tc = float(alumno.get('imp_tc', 0) or 0)
if imp_tc <= 0: imp_tc = 3.45 # TC por defecto si no hay comprobante
except: inv_neta, imp_tc = 0.0, 3.45
inv_neta_final = round(inv_neta * imp_tc if cod_moneda == 'DOL' else inv_neta, 2)
categoria_json = self.obtener_categoria_programa(nombre_programa)
es_descuento = False
if categoria_json in ["AREQUIPA", "TRUJILLO", "PIURA"] and 1200 <= inv_neta_final <= 1700: es_descuento = True
elif categoria_json in ["TEAC", "TERC"] and 1400 <= inv_neta_final <= 1900: es_descuento = True
if es_descuento:
conteo_por_curso[num_indice] = conteo_por_curso.get(num_indice, 0) + 1
alumnos_refriperu.add(mat_id)
except: continue
for curso in datos: curso['Descuento'] = conteo_por_curso.get(str(curso.get('num_indice', '')), 0)
return datos, alumnos_refriperu
# Lógica estricta de Continuidad
def agregar_continuidad(self, datos, datos_raw_matriculas, datos_historial, alumnos_refriperu):
conteo_por_curso = {}
correcciones_continuidad = self.data_manager.config_data.get('correcciones_continuidad', {})
if datos_raw_matriculas and datos_historial:
# PRE-ÍNDICE: {dni: [(linea_carrera, fecha), ...]}
# Calculamos identificar_linea_carrera UNA sola vez por programa del historial
historial_indexado = {}
for dni, antecedentes in datos_historial.items():
lineas = []
for ant in antecedentes:
linea = self.identificar_linea_carrera(str(ant.get('programa', '')))
fecha = ant.get('fecha')
if linea and fecha:
lineas.append((linea, fecha))
if lineas:
historial_indexado[dni] = lineas
alumnos_por_curso = {}
for alu in datos_raw_matriculas:
idx = str(alu.get('num_indice', ''))
if idx not in alumnos_por_curso: alumnos_por_curso[idx] = []
alumnos_por_curso[idx].append(alu)
for curso in datos:
num_indice = str(curso.get('num_indice', ''))
fecha_inicio_actual = self.parse_fecha(curso.get('fch_inicio'))
linea_actual = self.identificar_linea_carrera(str(curso.get('dsc_programa', '')))
if not linea_actual or not fecha_inicio_actual: continue
contador_fieles = 0
for alumno in alumnos_por_curso.get(num_indice, []):
if str(alumno.get('estado_matricula', '')).strip().upper() not in ['ALU', 'PRE']: continue
mat_id = str(alumno.get('num_matricula', '')).strip()
if mat_id.endswith('.0'): mat_id = mat_id[:-2]
if mat_id in alumnos_refriperu: continue
dni = str(alumno.get('dsc_documento', 'SD')).strip()
es_fiel = False
if mat_id in correcciones_continuidad:
accion = str(correcciones_continuidad[mat_id]).upper().strip()
if accion == "SI": es_fiel = True
elif accion == "NO": continue
if not es_fiel and dni in historial_indexado:
for linea_pasada, fecha_pasada in historial_indexado[dni]:
if fecha_pasada >= fecha_inicio_actual: continue
if (fecha_inicio_actual - fecha_pasada).days < 60: continue
if linea_pasada == linea_actual:
es_fiel = True
break
if es_fiel: contador_fieles += 1
conteo_por_curso[num_indice] = contador_fieles
for curso in datos: curso['Inscritos_Continuidad'] = conteo_por_curso.get(str(curso.get('num_indice', '')), 0)
return datos
def agregar_inscritos_mes(self, datos, datos_matriculas):
dict_norm = {str(k): v for k, v in datos_matriculas.items()}
for curso in datos: curso['Inscritos_Mes'] = dict_norm.get(str(curso.get('num_indice', '')), 0)
return datos
def agregar_inscritos_pc(self, datos, datos_raw_matriculas):
conteo_pc = {}
if datos_raw_matriculas:
for m in datos_raw_matriculas:
try:
if str(m.get('estado_matricula', '')).strip() in ['ALU', 'PRE'] and float(m.get('imp_saldo_matricula', 0) or 0) < 1 and float(m.get('imp_saldo_cuota1', 0) or 0) < 1:
nid = str(m.get('num_indice', ''))
conteo_pc[nid] = conteo_pc.get(nid, 0) + 1
except: continue
for curso in datos: curso['Inscritos_PC'] = conteo_pc.get(str(curso.get('num_indice', '')), 0)
return datos