795 lines
37 KiB
Python
795 lines
37 KiB
Python
# core/data_manager.py
|
|
import os
|
|
import requests
|
|
import json
|
|
from datetime import datetime
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
class DataManager:
|
|
def __init__(self):
|
|
# --- SQL SERVER ---
|
|
self.server = os.getenv("SQL_SERVER", "191.98.134.80")
|
|
self.database = os.getenv("SQL_DATABASE", "BDUS_CK000040_0001")
|
|
self.username = os.getenv("SQL_USERNAME", "ASEBASTIAN")
|
|
self.password = os.getenv("SQL_PASSWORD", "")
|
|
|
|
# --- POSTGRESQL ---
|
|
self.pg_host = os.getenv("PG_HOST", "191.98.134.81")
|
|
self.pg_database = os.getenv("PG_DATABASE", "chatwoot_production")
|
|
self.pg_user = os.getenv("PG_USER", "postgres")
|
|
self.pg_password = os.getenv("PG_PASSWORD", "")
|
|
self.pg_port = os.getenv("PG_PORT", "5432")
|
|
|
|
# --- SUPABASE ---
|
|
self.supabase_url = os.getenv("SUPABASE_URL", "")
|
|
self.supabase_key = os.getenv("SUPABASE_KEY", "")
|
|
self.supabase_client = None
|
|
self._init_supabase()
|
|
|
|
# --- GITHUB ---
|
|
base = os.getenv("GITHUB_BASE", "https://raw.githubusercontent.com/aron1798/prueba1/main/DASHBOARD")
|
|
self.github_json_url = f"{base}/actualizador.json"
|
|
self.github_replace_url = f"{base}/REPLACE_CURSO.json"
|
|
self.github_meta_url = f"{base}/REPLACE_META.json"
|
|
self.github_costos_url = f"{base}/REPLACE_COSTOS.json"
|
|
self.github_pronostico_url = f"{base}/BASE_PRONOSTICO.json"
|
|
self.github_pendientes_url = f"{base}/BASE_PENDIENTES.json"
|
|
self.github_sede_url = f"{base}/sede.json"
|
|
self.github_horario_url = f"{base}/HORARIO_ASESOR.json"
|
|
self.github_query_url = f"{base}/SQL_QUERY_2/BASE_CURSO_2.query"
|
|
self.github_cuota_url = f"{base}/SQL_QUERY_2/BASE_CUOTAS_2.query"
|
|
self.github_cronograma_url = f"{base}/SQL_QUERY_2/BASE_CRONOGRAMA_2.query"
|
|
self.github_facturas_url = f"{base}/SQL_QUERY_2/BASE_FACTURAS_2.query"
|
|
self.github_matriculas_url = f"{base}/SQL_QUERY_2/BASE_MATRICULADOS_2.query"
|
|
|
|
# --- Variables de estado ---
|
|
self.config_data = {}
|
|
self.replace_data = {}
|
|
self.meta_data = {}
|
|
self.sede_data = {}
|
|
self.pronostico_data = {}
|
|
self.costos_data = {}
|
|
self.historico_pendientes = {}
|
|
self.pendientes_data = set()
|
|
self.query_sql = ""
|
|
self.query_matriculas_sql = ""
|
|
self.query_cronograma_sql = ""
|
|
self.query_facturas_sql = ""
|
|
|
|
self.cargar_toda_configuracion()
|
|
|
|
# =========================================================================
|
|
# CARGA DE CONFIGURACIÓN
|
|
# =========================================================================
|
|
def cargar_toda_configuracion(self):
|
|
# OPTIMIZACIÓN: descargar todas las URLs en paralelo
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
json_tasks = {
|
|
"config_data": (self.github_json_url, {"config_general": {"auto_update_minutos": 5}}),
|
|
"replace_data": (self.github_replace_url, {}),
|
|
"meta_data": (self.github_meta_url, {}),
|
|
"sede_data": (self.github_sede_url, {}),
|
|
"pronostico_data": (self.github_pronostico_url, {"pronosticos": []}),
|
|
"costos_data": (self.github_costos_url, {}),
|
|
"_pendientes_raw": (self.github_pendientes_url, {}),
|
|
}
|
|
text_tasks = {
|
|
"query_sql": self.github_query_url,
|
|
"query_matriculas_sql": self.github_matriculas_url,
|
|
"query_cronograma_sql": self.github_cronograma_url,
|
|
"query_facturas_sql": self.github_facturas_url,
|
|
}
|
|
with ThreadPoolExecutor(max_workers=15) as ex:
|
|
json_futs = {k: ex.submit(self._get_json, url, default) for k,(url,default) in json_tasks.items()}
|
|
text_futs = {k: ex.submit(self._get_text, url) for k,url in text_tasks.items()}
|
|
for k, f in json_futs.items(): setattr(self, k, f.result())
|
|
for k, f in text_futs.items(): setattr(self, k, f.result())
|
|
# pendientes: parsear el dict ya descargado
|
|
try:
|
|
self.historico_pendientes = self._pendientes_raw.get("historico_pendientes", {})
|
|
except:
|
|
self.historico_pendientes = {}
|
|
self.pendientes_data = set()
|
|
|
|
def _get_json(self, url, default=None):
|
|
try:
|
|
r = requests.get(url, timeout=4)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except:
|
|
return default or {}
|
|
|
|
def _get_text(self, url):
|
|
try:
|
|
r = requests.get(url, timeout=4)
|
|
r.raise_for_status()
|
|
return r.text
|
|
except:
|
|
return ""
|
|
|
|
def _cargar_pendientes(self):
|
|
# Ya cargado en paralelo dentro de cargar_toda_configuracion
|
|
if not hasattr(self, 'historico_pendientes'):
|
|
self.historico_pendientes = {}
|
|
if not hasattr(self, 'pendientes_data'):
|
|
self.pendientes_data = set()
|
|
|
|
# =========================================================================
|
|
# CONEXIONES A BASE DE DATOS
|
|
# =========================================================================
|
|
def get_connection(self):
|
|
import pyodbc
|
|
conn_str = (
|
|
f"DRIVER={{ODBC Driver 17 for SQL Server}};" # <--- ¡ESTE ES EL CAMBIO MÁGICO!
|
|
f"SERVER={self.server};"
|
|
f"DATABASE={self.database};"
|
|
f"UID={self.username};"
|
|
f"PWD={self.password}"
|
|
)
|
|
return pyodbc.connect(conn_str)
|
|
|
|
def get_pg_connection(self):
|
|
try:
|
|
import psycopg2
|
|
except ImportError:
|
|
raise ImportError(
|
|
"psycopg2 no está instalado. Para usar PostgreSQL ejecuta:\n"
|
|
"pip install psycopg2-binary"
|
|
)
|
|
try:
|
|
return psycopg2.connect(
|
|
host=self.pg_host,
|
|
database=self.pg_database,
|
|
user=self.pg_user,
|
|
password=self.pg_password,
|
|
port=self.pg_port
|
|
)
|
|
except Exception as e:
|
|
print(f"❌ Error PostgreSQL: {e}")
|
|
return None
|
|
|
|
def _init_supabase(self):
|
|
self.supabase_error = None
|
|
try:
|
|
from supabase import create_client
|
|
if self.supabase_url and self.supabase_key:
|
|
self.supabase_client = create_client(self.supabase_url, self.supabase_key)
|
|
print(f"✅ Supabase conectado: {self.supabase_url[:30]}...")
|
|
else:
|
|
self.supabase_error = "SUPABASE_URL o SUPABASE_KEY vacíos"
|
|
print(f"⚠️ {self.supabase_error}")
|
|
except Exception as e:
|
|
self.supabase_client = None
|
|
self.supabase_error = f"{type(e).__name__}: {e}"
|
|
print(f"❌ Error al conectar Supabase: {self.supabase_error}")
|
|
|
|
# =========================================================================
|
|
# CONSULTAS SQL
|
|
# =========================================================================
|
|
def ejecutar_consulta_cursos(self, ano, mes):
|
|
try:
|
|
if not self.query_sql:
|
|
raise Exception("No se pudo cargar la query BASE_CURSO")
|
|
|
|
ids_invitados = []
|
|
cursos_personalizados = self.config_data.get('cursos_personalizados', {})
|
|
for id_curso, datos in cursos_personalizados.items():
|
|
if 'fch_inicio' in datos:
|
|
try:
|
|
f_obj = datetime.strptime(datos['fch_inicio'], '%d-%m-%Y')
|
|
if str(f_obj.year) == str(ano) and str(f_obj.month) == str(mes):
|
|
ids_invitados.append(str(int(id_curso)))
|
|
except:
|
|
pass
|
|
|
|
sql_final = self.query_sql
|
|
if ids_invitados:
|
|
ids_str = ",".join(ids_invitados)
|
|
if "ORDER BY" in sql_final:
|
|
parts = sql_final.split("ORDER BY")
|
|
sql_final = parts[0] + f" OR rp.num_indice IN ({ids_str}) \nORDER BY" + parts[1]
|
|
else:
|
|
sql_final += f" OR rp.num_indice IN ({ids_str})"
|
|
|
|
conn = self.get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute(sql_final, ano, mes)
|
|
rows = cursor.fetchall()
|
|
cols = ['num_indice', 'dsc_det_programa', 'flg_activo', 'fch_inicio',
|
|
'cod_frecuencia', 'cod_estado', 'dsc_programa',
|
|
'Inscritos_Totales', 'Inscritos_Retirados', 'Inscritos_Activos']
|
|
datos = [{cols[i]: row[i] for i in range(len(cols))} for row in rows]
|
|
conn.close()
|
|
return datos
|
|
except Exception as e:
|
|
print(f"❌ Error ejecutar_consulta_cursos: {e}")
|
|
raise
|
|
|
|
def cargar_datos_matriculas(self, ano, mes):
|
|
try:
|
|
if not self.query_matriculas_sql:
|
|
return {}
|
|
conn = self.get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute(self.query_matriculas_sql, ano, mes)
|
|
rows = cursor.fetchall()
|
|
d = {}
|
|
for row in rows:
|
|
d[row[0]] = d.get(row[0], 0) + 1
|
|
conn.close()
|
|
return d
|
|
except:
|
|
return {}
|
|
|
|
def ejecutar_consulta_matriculados_detalle(self, ano, mes):
|
|
try:
|
|
conn = self.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
ids_invitados = []
|
|
cursos_personalizados = self.config_data.get('cursos_personalizados', {})
|
|
for id_curso, datos in cursos_personalizados.items():
|
|
if 'fch_inicio' in datos:
|
|
try:
|
|
f_obj = datetime.strptime(datos['fch_inicio'], '%d-%m-%Y')
|
|
if str(f_obj.year) == str(ano) and str(f_obj.month) == str(mes):
|
|
ids_invitados.append(str(int(id_curso)))
|
|
except:
|
|
pass
|
|
|
|
sql_or = ""
|
|
if ids_invitados:
|
|
sql_or = f" OR sgede_RP_programa.num_indice IN ({','.join(ids_invitados)}) "
|
|
|
|
query = f"""
|
|
SELECT
|
|
sgede_RP_programa.num_indice, sgeca_matricula.num_matricula,
|
|
sgeca_matricula.cod_estado AS estado_matricula,
|
|
sgeca_programa.dsc_programa, sgeca_matricula.cod_moneda,
|
|
sgema_alumno.dsc_documento,
|
|
ISNULL((SELECT SUM(sgede_cronograma_matricula.imp_total - ISNULL(sgede_cronograma_matricula.imp_dscto, 0))
|
|
FROM sgede_cronograma_matricula
|
|
WHERE sgede_cronograma_matricula.cod_localidad = sgeca_matricula.cod_localidad
|
|
AND sgede_cronograma_matricula.num_matricula = sgeca_matricula.num_matricula
|
|
AND sgede_cronograma_matricula.num_refinanciamiento = 1), 0) AS INV_NETA,
|
|
ISNULL((SELECT sgede_cronograma_matricula.imp_saldo FROM sgede_cronograma_matricula
|
|
WHERE sgede_cronograma_matricula.cod_localidad = sgeca_matricula.cod_localidad
|
|
AND sgede_cronograma_matricula.num_matricula = sgeca_matricula.num_matricula
|
|
AND sgede_cronograma_matricula.num_refinanciamiento = 1
|
|
AND sgede_cronograma_matricula.num_cuota = 0), 0) AS imp_saldo_matricula,
|
|
ISNULL((SELECT sgede_cronograma_matricula.imp_saldo FROM sgede_cronograma_matricula
|
|
WHERE sgede_cronograma_matricula.cod_localidad = sgeca_matricula.cod_localidad
|
|
AND sgede_cronograma_matricula.num_matricula = sgeca_matricula.num_matricula
|
|
AND sgede_cronograma_matricula.num_refinanciamiento = 1
|
|
AND sgede_cronograma_matricula.num_cuota = 1), 0) AS imp_saldo_cuota1,
|
|
ISNULL((SELECT TOP 1 comp.imp_tc FROM vtaca_comprobante comp
|
|
INNER JOIN sgevi_cuotas_x_comprobante cxc
|
|
ON comp.cod_localidad = cxc.cod_localidad
|
|
AND comp.num_correlativo = cxc.num_correlativo
|
|
WHERE cxc.num_matricula = sgeca_matricula.num_matricula
|
|
AND cxc.cod_localidad_c = sgeca_matricula.cod_localidad
|
|
AND comp.cod_estado NOT IN ('ANU', 'ELI')
|
|
ORDER BY comp.fch_emision DESC), 0) AS imp_tc
|
|
FROM sgeca_matricula
|
|
INNER JOIN sgeca_programa ON sgeca_matricula.cod_programa = sgeca_programa.cod_programa
|
|
LEFT JOIN sgede_RP_programa
|
|
ON sgeca_matricula.cod_periodo = sgede_RP_programa.cod_detalle
|
|
AND sgeca_matricula.cod_programa = sgede_RP_programa.cod_programa
|
|
AND sgeca_matricula.num_indice = sgede_RP_programa.num_indice
|
|
INNER JOIN sgema_alumno ON sgeca_matricula.cod_alumno = sgema_alumno.cod_alumno
|
|
WHERE sgeca_matricula.cod_localidad LIKE 'SCENT'
|
|
AND ((YEAR(sgede_RP_programa.fch_inicio) = ? AND MONTH(sgede_RP_programa.fch_inicio) = ?) {sql_or})
|
|
AND sgeca_matricula.cod_estado NOT IN ('ANU', 'SUS')
|
|
"""
|
|
cursor.execute(query, ano, mes)
|
|
columns = [col[0] for col in cursor.description]
|
|
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
|
conn.close()
|
|
return results
|
|
except Exception as e:
|
|
print(f"❌ Error ejecutar_consulta_matriculados_detalle: {e}")
|
|
return []
|
|
|
|
def consultar_historial_continuidad(self, lista_documentos):
|
|
if not lista_documentos:
|
|
return {}
|
|
try:
|
|
dnis = list(set([str(d).strip() for d in lista_documentos if d]))
|
|
if not dnis:
|
|
return {}
|
|
placeholders = ','.join(['?'] * len(dnis))
|
|
query = f"""
|
|
SELECT a.dsc_documento, p.dsc_programa, rp.fch_inicio
|
|
FROM sgeca_matricula m
|
|
INNER JOIN sgema_alumno a ON m.cod_alumno = a.cod_alumno
|
|
INNER JOIN sgeca_programa p ON m.cod_programa = p.cod_programa
|
|
INNER JOIN sgede_RP_programa rp
|
|
ON m.cod_periodo = rp.cod_detalle
|
|
AND m.cod_programa = rp.cod_programa
|
|
AND m.num_indice = rp.num_indice
|
|
WHERE a.dsc_documento IN ({placeholders})
|
|
AND m.cod_estado IN ('ALU', 'PRE', 'RET')
|
|
AND m.cod_localidad LIKE 'SCENT'
|
|
AND (p.dsc_programa LIKE '%TEAC%' OR p.dsc_programa LIKE '%TERC%'
|
|
OR p.dsc_programa LIKE '%AREQUIPA%' OR p.dsc_programa LIKE '%TRUJILLO%'
|
|
OR p.dsc_programa LIKE '%PIURA%' OR p.dsc_programa LIKE '%TECNICO ESPECIALISTA%')
|
|
"""
|
|
conn = self.get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute(query, dnis)
|
|
historial = {}
|
|
for row in cursor.fetchall():
|
|
dni = str(row[0]).strip()
|
|
if dni not in historial:
|
|
historial[dni] = []
|
|
historial[dni].append({'programa': row[1], 'fecha': row[2]})
|
|
conn.close()
|
|
return historial
|
|
except Exception as e:
|
|
print(f"❌ Error historial continuidad: {e}")
|
|
return {}
|
|
|
|
def _get_query_ventas_hibrido(self, lista_ids_vip):
|
|
if not lista_ids_vip:
|
|
sql_in_clause = "(-1)"
|
|
else:
|
|
ids_str = [str(x) for x in lista_ids_vip]
|
|
sql_in_clause = "(" + ",".join(ids_str) + ")"
|
|
|
|
return f"""
|
|
SELECT
|
|
(
|
|
SELECT
|
|
rhuma_trabajador.dsc_apellido_paterno + ' ' +
|
|
SUBSTRING(rhuma_trabajador.dsc_apellido_materno, 1, 1) + '. ' +
|
|
rhuma_trabajador.dsc_nombres
|
|
FROM rhuma_trabajador
|
|
WHERE rhuma_trabajador.cod_trabajador = sgeca_matricula.cod_vendedor
|
|
) AS dsc_vendedor, -- [0]
|
|
ISNULL((
|
|
SELECT
|
|
SUM(sgede_cronograma_matricula.imp_total - ISNULL(sgede_cronograma_matricula.imp_dscto, 0))
|
|
FROM sgede_cronograma_matricula
|
|
WHERE sgede_cronograma_matricula.cod_localidad = sgeca_matricula.cod_localidad
|
|
AND sgede_cronograma_matricula.num_matricula = sgeca_matricula.num_matricula
|
|
AND sgede_cronograma_matricula.num_refinanciamiento = 1
|
|
), 0) AS INV_NETA, -- [1]
|
|
ISNULL((
|
|
SELECT sgede_cronograma_matricula.imp_saldo
|
|
FROM sgede_cronograma_matricula
|
|
WHERE sgede_cronograma_matricula.cod_localidad = sgeca_matricula.cod_localidad
|
|
AND sgede_cronograma_matricula.num_matricula = sgeca_matricula.num_matricula
|
|
AND sgede_cronograma_matricula.num_refinanciamiento = 1
|
|
AND sgede_cronograma_matricula.num_cuota = 0
|
|
), 0) AS imp_saldo_matricula, -- [2]
|
|
ISNULL((
|
|
SELECT sgede_cronograma_matricula.imp_saldo
|
|
FROM sgede_cronograma_matricula
|
|
WHERE sgede_cronograma_matricula.cod_localidad = sgeca_matricula.cod_localidad
|
|
AND sgede_cronograma_matricula.num_matricula = sgeca_matricula.num_matricula
|
|
AND sgede_cronograma_matricula.num_refinanciamiento = 1
|
|
AND sgede_cronograma_matricula.num_cuota = 1
|
|
), 0) AS imp_saldo_cuota1, -- [3]
|
|
|
|
sgeca_matricula.num_matricula, -- [4]
|
|
YEAR(sgeca_matricula.fch_matricula) as anio_mat, -- [5]
|
|
MONTH(sgeca_matricula.fch_matricula) as mes_mat, -- [6]
|
|
|
|
(
|
|
SELECT TOP 1 sgede_cronograma_matricula.fch_cancelacion
|
|
FROM sgede_cronograma_matricula
|
|
WHERE sgede_cronograma_matricula.cod_localidad = sgeca_matricula.cod_localidad
|
|
AND sgede_cronograma_matricula.num_matricula = sgeca_matricula.num_matricula
|
|
AND sgede_cronograma_matricula.num_refinanciamiento = 1
|
|
AND sgede_cronograma_matricula.num_cuota = 1
|
|
) AS fch_cancelacion_cuota1, -- [7]
|
|
|
|
sgeca_matricula.cod_moneda, -- [8]
|
|
|
|
ISNULL((
|
|
SELECT TOP 1 comp.imp_tc
|
|
FROM vtaca_comprobante comp
|
|
INNER JOIN sgevi_cuotas_x_comprobante cxc ON comp.cod_localidad = cxc.cod_localidad
|
|
AND comp.num_correlativo = cxc.num_correlativo
|
|
WHERE cxc.num_matricula = sgeca_matricula.num_matricula
|
|
AND cxc.cod_localidad_c = sgeca_matricula.cod_localidad
|
|
AND comp.cod_estado NOT IN ('ANU', 'ELI')
|
|
ORDER BY comp.fch_emision DESC
|
|
), 1) AS imp_tc, -- [9]
|
|
|
|
sgede_RP_programa.dsc_det_programa, -- [10] PROGRAMA (mismo num_indice)
|
|
sgede_RP_programa.fch_inicio, -- [11] FECHA INICIO (mismo num_indice)
|
|
sgeca_programa.dsc_programa, -- [12] PROGRAMA GENERAL (respaldo)
|
|
sgeca_matricula.cod_estado -- [13] ESTADO (ALU/PRE/RET)
|
|
|
|
FROM sgeca_matricula
|
|
INNER JOIN sgeca_programa ON sgeca_matricula.cod_programa = sgeca_programa.cod_programa
|
|
LEFT JOIN sgede_RP_programa ON sgeca_matricula.cod_periodo = sgede_RP_programa.cod_detalle
|
|
AND sgeca_matricula.cod_programa = sgede_RP_programa.cod_programa
|
|
AND sgeca_matricula.num_indice = sgede_RP_programa.num_indice
|
|
INNER JOIN sgema_alumno ON sgeca_matricula.cod_alumno = sgema_alumno.cod_alumno
|
|
INNER JOIN vtama_localidad ON sgeca_matricula.cod_localidad = vtama_localidad.cod_localidad
|
|
INNER JOIN vtama_tipo_documento ON sgema_alumno.cod_tipo_documento = vtama_tipo_documento.cod_tipo_documento
|
|
WHERE
|
|
sgeca_matricula.cod_localidad LIKE 'SCENT'
|
|
AND (
|
|
sgeca_matricula.cod_estado IN ('ALU', 'PRE', 'RET')
|
|
OR sgeca_matricula.num_matricula IN {sql_in_clause}
|
|
)
|
|
AND (
|
|
(YEAR(sgeca_matricula.fch_matricula) = ? AND MONTH(sgeca_matricula.fch_matricula) = ?)
|
|
OR
|
|
sgeca_matricula.num_matricula IN {sql_in_clause}
|
|
OR
|
|
EXISTS (
|
|
SELECT 1 FROM sgede_cronograma_matricula crono
|
|
WHERE crono.cod_localidad = sgeca_matricula.cod_localidad
|
|
AND crono.num_matricula = sgeca_matricula.num_matricula
|
|
AND crono.num_cuota = 1
|
|
AND crono.fch_cancelacion IS NOT NULL
|
|
AND YEAR(crono.fch_cancelacion) = ?
|
|
AND MONTH(crono.fch_cancelacion) = ?
|
|
)
|
|
)
|
|
ORDER BY
|
|
dsc_vendedor ASC
|
|
"""
|
|
|
|
def ejecutar_consulta_ventas(self, ano, mes, sede="TODOS", programa="TODOS"):
|
|
try:
|
|
key_mes = f"{int(mes):02d}-{ano}"
|
|
lista_raw = self.historico_pendientes.get(key_mes, [])
|
|
|
|
lista_vip_list = []
|
|
for item in lista_raw:
|
|
try: lista_vip_list.append(int(item))
|
|
except: pass
|
|
|
|
# Incluir matrículas con override de fecha (para que la query las traiga, incluso ANU)
|
|
_fov = getattr(self, '_fecha_canc_overrides', None) or {}
|
|
for mk in _fov.keys():
|
|
try: lista_vip_list.append(int(float(str(mk))))
|
|
except: pass
|
|
|
|
correcciones = self.config_data.get("correcciones_matriculas", {})
|
|
query_hibrido = self._get_query_ventas_hibrido(lista_vip_list)
|
|
conn = self.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# Clasificadores para filtros (mismos que Cobranza/Ocupabilidad)
|
|
_sede_sel = str(sede or "TODOS").upper()
|
|
_prog_sel = str(programa or "TODOS").upper()
|
|
def _clasif_sede(dscp):
|
|
if not dscp: return "LIMA"
|
|
up = str(dscp).upper()
|
|
sedes_data = self.sede_data.get("clasificacion_sedes", {})
|
|
for s, data in sedes_data.items():
|
|
if s == "DEFAULT": continue
|
|
for pat in data.get("patrones", []):
|
|
if pat.upper() in up: return s
|
|
_def = self.sede_data.get("clasificacion_default", {})
|
|
return _def.get("sede", "LIMA") if isinstance(_def, dict) else "LIMA"
|
|
def _clasif_prog(dscp):
|
|
# Clasifica el programa en UNA sola categoría, con prioridad de orden.
|
|
# Específicas primero (SEMINARIOS, TEAC, TERC); OTROS y el sobrante al final.
|
|
up = str(dscp or "").upper()
|
|
fdata = self.meta_data.get("clasificacion_filtro_programa", {})
|
|
orden = ["OTROS", "SEMINARIOS", "TEAC", "TERC"]
|
|
for cat in orden:
|
|
data = fdata.get(cat, {})
|
|
for pat in data.get("patrones", []):
|
|
if pat.upper() in up:
|
|
return cat
|
|
# Sobrante (no coincide con nada) → DEFAULT
|
|
return fdata.get("DEFAULT", "OTROS") if isinstance(fdata.get("DEFAULT"), str) else "OTROS"
|
|
|
|
cursor.execute(query_hibrido, ano, mes, ano, mes)
|
|
rows = cursor.fetchall()
|
|
|
|
ventas_por_vendedor = {}
|
|
set_vip_actual = set(lista_vip_list)
|
|
|
|
vendedores_excluidos = {
|
|
"CASTILLO B. KARINA LISSET",
|
|
"CASTILLO B. KARINA LISSET",
|
|
"CALDERON S. LISSA GENA",
|
|
"CRUZ G. FIORELLA MELISSA",
|
|
"URIBE G. MARIA MERCEDES"
|
|
}
|
|
|
|
for row in rows:
|
|
vendedor = row[0] if row[0] else "SIN VENDEDOR"
|
|
if vendedor in vendedores_excluidos: continue
|
|
|
|
# Filtro SEDE/PROGRAMA (mismas reglas que Ocupabilidad/Cobranza)
|
|
_dscp = row[10] if (len(row) > 10 and row[10]) else (row[12] if len(row) > 12 else "")
|
|
if _sede_sel != "TODOS" and _clasif_sede(_dscp) != _sede_sel:
|
|
continue
|
|
if _prog_sel != "TODOS" and _clasif_prog(_dscp) != _prog_sel:
|
|
continue
|
|
|
|
inv_neta_raw = float(row[1]) if row[1] else 0.0
|
|
cod_moneda = row[8]
|
|
imp_tc = float(row[9]) if row[9] else 1.0
|
|
|
|
saldo_mat = float(row[2]) if row[2] else 0.0
|
|
saldo_c1 = float(row[3]) if row[3] else 0.0
|
|
|
|
raw_mat = row[4]
|
|
row_year = row[5]
|
|
row_month = row[6]
|
|
fch_cancelacion_raw = row[7]
|
|
|
|
matricula_int = -1
|
|
matricula_str = ""
|
|
try:
|
|
if raw_mat is not None:
|
|
val_str = str(raw_mat).strip()
|
|
if val_str.endswith('.0'): val_str = val_str[:-2]
|
|
matricula_str = val_str
|
|
matricula_int = int(float(val_str))
|
|
except: pass
|
|
|
|
if matricula_str in correcciones:
|
|
datos_corregidos = correcciones[matricula_str]
|
|
if "imp_tc" in datos_corregidos: imp_tc = float(datos_corregidos["imp_tc"])
|
|
if "imp_saldo_cuota1" in datos_corregidos: saldo_c1 = float(datos_corregidos["imp_saldo_cuota1"])
|
|
if "imp_saldo_matricula" in datos_corregidos: saldo_mat = float(datos_corregidos["imp_saldo_matricula"])
|
|
if "fch_cancelacion_cuota1" in datos_corregidos: fch_cancelacion_raw = datos_corregidos["fch_cancelacion_cuota1"]
|
|
|
|
# Override de FECHA CANCELACIÓN 1 desde Supabase (prioridad para clasificación)
|
|
_fov = getattr(self, '_fecha_canc_overrides', None)
|
|
_vacio_forzado = False
|
|
if _fov and matricula_str in _fov:
|
|
fov_val = _fov[matricula_str]
|
|
s = str(fov_val).strip()
|
|
if s == "__VACIO__":
|
|
# Fecha borrada a propósito → anula la del SQL (no cuenta como pagado)
|
|
fch_cancelacion_raw = None
|
|
_vacio_forzado = True
|
|
else:
|
|
# Normalizar dd/mm/yyyy o d/m/yyyy → yyyy-mm-dd
|
|
if '/' in s:
|
|
p = s.split('/')
|
|
if len(p) == 3:
|
|
s = f"{p[2]}-{int(p[1]):02d}-{int(p[0]):02d}"
|
|
fch_cancelacion_raw = s
|
|
|
|
# Prioridad TC: Supabase (mes) > corrección manual > SQL
|
|
_tc_ov = getattr(self, '_tc_override_mes', None)
|
|
if _tc_ov: imp_tc = float(_tc_ov)
|
|
|
|
if cod_moneda == 'DOL': inv_neta = inv_neta_raw * imp_tc
|
|
else: inv_neta = inv_neta_raw
|
|
|
|
# Override de INVERSIÓN NETA (Supabase): monto final en soles, reemplaza al SQL.
|
|
_inv_ov = getattr(self, '_inv_neta_overrides', None)
|
|
if _inv_ov and matricula_str in _inv_ov:
|
|
inv_neta = float(_inv_ov[matricula_str])
|
|
|
|
if vendedor not in ventas_por_vendedor:
|
|
ventas_por_vendedor[vendedor] = {
|
|
'monto': 0.0, 'cantidad': 0, 'monto_pc': 0.0, 'cantidad_pc': 0,
|
|
'monto_pendientes': 0.0, 'cantidad_pendientes': 0
|
|
}
|
|
|
|
es_venta_del_mes = (str(row_year) == str(ano)) and (str(row_month) == str(mes))
|
|
saldos_ok = (saldo_mat == 0 and saldo_c1 == 0)
|
|
|
|
# Si hay override de fecha de cancelación 1 (con fecha), se considera PAGADO.
|
|
# Si el override la vació a propósito, NO se fuerza pagado.
|
|
tiene_override_fecha = bool(_fov and matricula_str in _fov) and not _vacio_forzado
|
|
if tiene_override_fecha:
|
|
saldos_ok = True
|
|
|
|
# Si es RET y NO pagó (sin override ni saldos), NO entra en la tabla principal.
|
|
estado_row = str(row[13]).strip().upper() if len(row) > 13 and row[13] else ""
|
|
if estado_row == "RET" and not saldos_ok:
|
|
continue
|
|
|
|
# ¿Pagó la 1° cuota en el MES del filtro?
|
|
pago_en_fecha_correcta = False
|
|
if fch_cancelacion_raw:
|
|
try:
|
|
if isinstance(fch_cancelacion_raw, str):
|
|
f_obj = datetime.strptime(fch_cancelacion_raw[:10], '%Y-%m-%d')
|
|
f_ano, f_mes = f_obj.year, f_obj.month
|
|
else:
|
|
f_ano, f_mes = fch_cancelacion_raw.year, fch_cancelacion_raw.month
|
|
if str(f_ano) == str(ano) and str(f_mes) == str(mes):
|
|
pago_en_fecha_correcta = True
|
|
except: pass
|
|
|
|
# ¿La matrícula es de un mes ANTERIOR al filtro? (mismo año mes menor, o año anterior)
|
|
matricula_mes_pasado = False
|
|
try:
|
|
ym_mat = int(row_year) * 100 + int(row_month)
|
|
ym_filtro = int(ano) * 100 + int(mes)
|
|
matricula_mes_pasado = ym_mat < ym_filtro
|
|
except: pass
|
|
|
|
# COLUMNA 2 y 3 — MES EN CURSO: matrícula del mes + pagó en el mes
|
|
if es_venta_del_mes:
|
|
ventas_por_vendedor[vendedor]['monto'] += inv_neta
|
|
ventas_por_vendedor[vendedor]['cantidad'] += 1
|
|
if saldos_ok and pago_en_fecha_correcta:
|
|
ventas_por_vendedor[vendedor]['monto_pc'] += inv_neta
|
|
ventas_por_vendedor[vendedor]['cantidad_pc'] += 1
|
|
|
|
# COLUMNA 4 y 5 — MES PASADO: matrícula anterior + pagó 1° cuota en el mes del filtro
|
|
if matricula_mes_pasado and saldos_ok and pago_en_fecha_correcta:
|
|
ventas_por_vendedor[vendedor]['monto_pendientes'] += inv_neta
|
|
ventas_por_vendedor[vendedor]['cantidad_pendientes'] += 1
|
|
|
|
datos_ventas = []
|
|
for vendedor, datos in ventas_por_vendedor.items():
|
|
datos_ventas.append({
|
|
'VENDEDOR': vendedor,
|
|
'MONTO': round(datos['monto'], 2),
|
|
'CANTIDAD': datos['cantidad'],
|
|
'VENTAS_PC': round(datos['monto_pc'], 2),
|
|
'INSCRITOS_PC': datos['cantidad_pc'],
|
|
'PENDIENTES': round(datos['monto_pendientes'], 2),
|
|
'INSCRITOS_PENDIENTES': datos['cantidad_pendientes']
|
|
})
|
|
|
|
datos_ventas.sort(key=lambda x: x['MONTO'], reverse=True)
|
|
conn.close()
|
|
return datos_ventas
|
|
except Exception as e:
|
|
return []
|
|
|
|
def ejecutar_consulta_cronograma_cobranza(self, ano):
|
|
try:
|
|
if not self.query_cronograma_sql:
|
|
return []
|
|
conn = self.get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute(self.query_cronograma_sql)
|
|
columns = [col[0] for col in cursor.description]
|
|
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
|
conn.close()
|
|
return results
|
|
except Exception as e:
|
|
print(f"❌ Error cronograma cobranza: {e}")
|
|
return []
|
|
|
|
def ejecutar_consulta_facturas_cobranza(self, ano):
|
|
try:
|
|
if not self.query_facturas_sql:
|
|
return []
|
|
conn = self.get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute(self.query_facturas_sql)
|
|
columns = [col[0] for col in cursor.description]
|
|
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
|
conn.close()
|
|
return results
|
|
except Exception as e:
|
|
print(f"❌ Error facturas cobranza: {e}")
|
|
return []
|
|
|
|
def ejecutar_consulta_saldos_anual(self, ano):
|
|
try:
|
|
if not self.query_matriculas_sql:
|
|
return []
|
|
sql_anual = self.query_matriculas_sql.replace(
|
|
"AND MONTH(sgeca_matricula.fch_matricula) = ?", ""
|
|
)
|
|
conn = self.get_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute(sql_anual, ano)
|
|
columns = [col[0] for col in cursor.description]
|
|
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
|
conn.close()
|
|
return results
|
|
except Exception as e:
|
|
print(f"❌ Error saldos anual: {e}")
|
|
return []
|
|
|
|
# =========================================================================
|
|
# SUPABASE - OVERRIDES DE COSTOS
|
|
# =========================================================================
|
|
def cargar_overrides_costos(self):
|
|
if not self.supabase_client:
|
|
return {}
|
|
try:
|
|
response = self.supabase_client.table('costos_overrides').select('*').execute()
|
|
overrides = {}
|
|
for row in response.data:
|
|
idx = str(row.get('num_indice', '')).strip()
|
|
tipo = str(row.get('tipo_costo', '')).strip().lower()
|
|
if not idx or tipo not in ('inicial', 'actual'):
|
|
continue
|
|
if idx not in overrides:
|
|
overrides[idx] = {}
|
|
overrides[idx][tipo] = {
|
|
'epp': row.get('costo_epp'),
|
|
'certificado': row.get('costo_certificado'),
|
|
'docente': row.get('costo_docente'),
|
|
'consumibles': row.get('costo_consumibles'),
|
|
'marketing': row.get('costo_marketing'),
|
|
}
|
|
return overrides
|
|
except Exception as e:
|
|
print(f"⚠️ Error overrides Supabase: {e}")
|
|
return {}
|
|
|
|
|
|
def obtener_fechas_originales(self):
|
|
"""Descarga todas las fechas originales guardadas en Supabase (inicios reprogramados)."""
|
|
if not self.supabase_client:
|
|
return {}
|
|
try:
|
|
respuesta = self.supabase_client.table('fechas_originales_cursos').select('*').execute()
|
|
datos = respuesta.data
|
|
diccionario_fechas = {}
|
|
for fila in datos:
|
|
indice = str(fila.get('num_indice'))
|
|
fecha = fila.get('fecha_inicio_original')
|
|
programa = fila.get('programa', 'CURSO REPROGRAMADO')
|
|
diccionario_fechas[indice] = {'fecha': fecha, 'programa': programa}
|
|
return diccionario_fechas
|
|
except Exception as e:
|
|
print(f"Error al cargar fechas originales: {e}")
|
|
return {}
|
|
|
|
def guardar_fecha_original(self, num_indice, programa, fecha_inicio):
|
|
"""Guarda silenciosamente un nuevo curso y su fecha inicial en Supabase."""
|
|
if not self.supabase_client:
|
|
return False
|
|
try:
|
|
payload = {
|
|
'num_indice': str(num_indice),
|
|
'programa': str(programa),
|
|
'fecha_inicio_original': str(fecha_inicio)
|
|
}
|
|
self.supabase_client.table('fechas_originales_cursos').upsert(payload).execute()
|
|
return True
|
|
except Exception as e:
|
|
print(f"Error al guardar fecha original: {e}")
|
|
return False
|
|
|
|
def guardar_override_costo(self, num_indice, tipo_costo, costos_dict):
|
|
if not self.supabase_client:
|
|
return False
|
|
try:
|
|
payload = {
|
|
'num_indice': str(num_indice),
|
|
'tipo_costo': tipo_costo,
|
|
'costo_epp': costos_dict.get('epp'),
|
|
'costo_certificado': costos_dict.get('certificado'),
|
|
'costo_docente': costos_dict.get('docente'),
|
|
'costo_consumibles': costos_dict.get('consumibles'),
|
|
'costo_marketing': costos_dict.get('marketing'),
|
|
'fecha_actualizacion': datetime.now().isoformat(),
|
|
}
|
|
self.supabase_client.table('costos_overrides').upsert(payload).execute()
|
|
return True
|
|
except Exception as e:
|
|
print(f"❌ Error guardar override: {e}")
|
|
return False
|
|
|
|
def eliminar_overrides_costo(self, num_indice):
|
|
if not self.supabase_client:
|
|
return False
|
|
try:
|
|
self.supabase_client.table('costos_overrides').delete().eq(
|
|
'num_indice', str(num_indice)
|
|
).execute()
|
|
return True
|
|
except Exception as e:
|
|
print(f"❌ Error eliminar overrides: {e}")
|
|
return False
|
|
|
|
# =========================================================================
|
|
# HELPERS
|
|
# =========================================================================
|
|
def get_current_year(self):
|
|
return datetime.now().year
|
|
|
|
def get_current_month(self):
|
|
return datetime.now().month
|