290 lines
14 KiB
Python
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 |