992 lines
48 KiB
Python
992 lines
48 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"
|
||
self.github_notascredito_url = f"{base}/SQL_QUERY_2/BASE_NOTASCREDITO_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.query_notascredito_sql = ""
|
||
|
||
# Caché en memoria de las consultas ANUALES de cobranza. Evita re-ejecutar
|
||
# la consulta del año COMPLETO en cada combinación de filtros (mes/sectorista/
|
||
# agrupación), que era lo que saturaba el SQL Server. Se llena solo cuando la
|
||
# consulta tiene éxito y se limpia en cada refresco (manual o ciclo 15 min).
|
||
self._cache_cronograma = {} # str(ano) -> filas
|
||
self._cache_facturas = {} # str(ano) -> filas
|
||
self._cache_notascredito = {} # str -> filas (rango completo)
|
||
|
||
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,
|
||
"query_notascredito_sql": self.github_notascredito_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={{SQL Server}};"
|
||
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", excluir_ret=False):
|
||
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
|
||
|
||
# Lista PURA de BASE_PENDIENTES (sin overrides). Venta Pendientes usa SOLO esto.
|
||
set_lista_pendientes = set(lista_vip_list)
|
||
|
||
# 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).
|
||
# Acepta varios valores separados por coma (multi-selección). Vacío/TODOS = todo.
|
||
_sede_set = {s.strip().upper() for s in str(sede or "TODOS").split(",") if s.strip()}
|
||
_prog_set = {p.strip().upper() for p in str(programa or "TODOS").split(",") if p.strip()}
|
||
_sede_all = (not _sede_set) or ("TODOS" in _sede_set)
|
||
_prog_all = (not _prog_set) or ("TODOS" in _prog_set)
|
||
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).
|
||
# Multi-selección: pasa si su sede/programa está en el set marcado.
|
||
_dscp = row[10] if (len(row) > 10 and row[10]) else (row[12] if len(row) > 12 else "")
|
||
if not _sede_all and _clasif_sede(_dscp) not in _sede_set:
|
||
continue
|
||
if not _prog_all and _clasif_prog(_dscp) not in _prog_set:
|
||
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
|
||
# En VENTAS (excluir_ret): los RET NO cuentan aunque hayan pagado.
|
||
if excluir_ret and estado_row == "RET":
|
||
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
|
||
|
||
# VENTA PENDIENTES: SOLO de tu lista BASE_PENDIENTES y que pagaron su 1ª
|
||
# cuota EN EL MES/AÑO DEL FILTRO (pago_en_fecha_correcta). Si pagó otro mes, no.
|
||
if matricula_int in set_lista_pendientes 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):
|
||
clave = str(ano)
|
||
en_cache = self._cache_cronograma.get(clave)
|
||
if en_cache is not None:
|
||
return en_cache
|
||
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()
|
||
self._cache_cronograma[clave] = results # cachear SOLO si tuvo éxito
|
||
return results
|
||
except Exception as e:
|
||
print(f"❌ Error cronograma cobranza: {e}")
|
||
return []
|
||
|
||
def ejecutar_consulta_facturas_cobranza(self, ano):
|
||
clave = str(ano)
|
||
en_cache = self._cache_facturas.get(clave)
|
||
if en_cache is not None:
|
||
return en_cache
|
||
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()
|
||
self._cache_facturas[clave] = results # cachear SOLO si tuvo éxito
|
||
return results
|
||
except Exception as e:
|
||
print(f"❌ Error facturas cobranza: {e}")
|
||
return []
|
||
|
||
def ejecutar_consulta_notascredito(self):
|
||
"""Notas de crédito (rango completo, cacheado). El query no tiene año."""
|
||
en_cache = self._cache_notascredito.get("all")
|
||
if en_cache is not None:
|
||
return en_cache
|
||
try:
|
||
if not self.query_notascredito_sql:
|
||
return []
|
||
conn = self.get_connection()
|
||
cursor = conn.cursor()
|
||
cursor.execute(self.query_notascredito_sql)
|
||
columns = [col[0] for col in cursor.description]
|
||
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||
conn.close()
|
||
self._cache_notascredito["all"] = results
|
||
return results
|
||
except Exception as e:
|
||
print(f"❌ Error notas de crédito: {e}")
|
||
return []
|
||
|
||
def traer_vendedor_cod_detalle(self, matriculas, ano=None, mes=None):
|
||
"""Dada una lista de num_matricula (POR RETIRAR, pocas), devuelve por cada una:
|
||
{num_matricula, dsc_vendedor, cod_detalle, ...}. Consulta ligera.
|
||
Las cuotas vencidas se cuentan HASTA EL FIN DEL MES (ano, mes) del filtro,
|
||
NO hasta la fecha de hoy (así septiembre no cambia al llegar octubre).
|
||
Si no se pasa ano/mes, usa la fecha de hoy (comportamiento anterior)."""
|
||
mats = [str(m).strip() for m in (matriculas or []) if str(m).strip()]
|
||
if not mats:
|
||
return {}
|
||
# Fecha de corte: primer día del mes SIGUIENTE al filtro (vencidas < corte).
|
||
# ini_mes = primer día del mes del filtro (para separar Anterior vs Curso).
|
||
corte_venc = "CAST(GETDATE() AS DATE)"
|
||
ini_mes = "CAST(GETDATE() AS DATE)"
|
||
if ano and mes:
|
||
try:
|
||
_a, _m = int(ano), int(mes)
|
||
_na, _nm = (_a + 1, 1) if _m == 12 else (_a, _m + 1)
|
||
corte_venc = f"CAST('{_na:04d}-{_nm:02d}-01' AS DATE)"
|
||
ini_mes = f"CAST('{_a:04d}-{_m:02d}-01' AS DATE)"
|
||
except Exception:
|
||
corte_venc = "CAST(GETDATE() AS DATE)"
|
||
ini_mes = "CAST(GETDATE() AS DATE)"
|
||
try:
|
||
conn = self.get_connection()
|
||
cursor = conn.cursor()
|
||
placeholders = ",".join("?" for _ in mats)
|
||
sql = f"""
|
||
SELECT
|
||
m.num_matricula,
|
||
(SELECT t.dsc_apellido_paterno + ' ' +
|
||
SUBSTRING(t.dsc_apellido_materno, 1, 1) + '. ' + t.dsc_nombres
|
||
FROM rhuma_trabajador t
|
||
WHERE t.cod_trabajador = m.cod_vendedor) AS dsc_vendedor,
|
||
m.cod_periodo AS cod_detalle,
|
||
a.dsc_apellido_paterno + ' ' + a.dsc_apellido_materno + ', ' + a.dsc_nombres AS dsc_alumno,
|
||
(SELECT COUNT(1)
|
||
FROM sgede_cronograma_matricula c
|
||
WHERE c.cod_localidad = m.cod_localidad
|
||
AND c.num_matricula = m.num_matricula
|
||
AND c.num_refinanciamiento = m.num_refinanciamiento
|
||
AND c.cod_tipo_cuota = 'ARM'
|
||
AND c.cod_estado = 'CAN') AS num_cuotas_pagadas,
|
||
(SELECT COUNT(1)
|
||
FROM sgede_cronograma_matricula c
|
||
WHERE c.cod_localidad = m.cod_localidad
|
||
AND c.num_matricula = m.num_matricula
|
||
AND c.num_refinanciamiento = m.num_refinanciamiento
|
||
AND c.cod_tipo_cuota = 'ARM'
|
||
AND c.cod_estado <> 'CAN'
|
||
AND c.fch_vencimiento < {corte_venc}) AS num_cuotas_vencidas,
|
||
STUFF((SELECT ' - ' + CONVERT(varchar(10), c.fch_vencimiento, 103)
|
||
FROM sgede_cronograma_matricula c
|
||
WHERE c.cod_localidad = m.cod_localidad
|
||
AND c.num_matricula = m.num_matricula
|
||
AND c.num_refinanciamiento = m.num_refinanciamiento
|
||
AND c.cod_tipo_cuota = 'ARM'
|
||
AND c.cod_estado <> 'CAN'
|
||
AND c.fch_vencimiento < {corte_venc}
|
||
ORDER BY c.fch_vencimiento
|
||
FOR XML PATH('')), 1, 3, '') AS fch_venc_mas_antigua,
|
||
ISNULL((SELECT SUM(c.imp_total - ISNULL(c.imp_dscto,0) - ISNULL(c.imp_total_pagado,0))
|
||
FROM sgede_cronograma_matricula c
|
||
WHERE c.cod_localidad = m.cod_localidad
|
||
AND c.num_matricula = m.num_matricula
|
||
AND c.num_refinanciamiento = m.num_refinanciamiento
|
||
AND c.cod_tipo_cuota = 'ARM'
|
||
AND c.cod_estado <> 'CAN'
|
||
AND c.fch_vencimiento < {ini_mes}), 0) AS monto_venc_ant,
|
||
ISNULL((SELECT SUM(c.imp_total - ISNULL(c.imp_dscto,0) - ISNULL(c.imp_total_pagado,0))
|
||
FROM sgede_cronograma_matricula c
|
||
WHERE c.cod_localidad = m.cod_localidad
|
||
AND c.num_matricula = m.num_matricula
|
||
AND c.num_refinanciamiento = m.num_refinanciamiento
|
||
AND c.cod_tipo_cuota = 'ARM'
|
||
AND c.cod_estado <> 'CAN'
|
||
AND c.fch_vencimiento >= {ini_mes}
|
||
AND c.fch_vencimiento < {corte_venc}), 0) AS monto_venc_cur,
|
||
(SELECT SUM(c.imp_total_pagado)
|
||
FROM sgede_cronograma_matricula c
|
||
WHERE c.cod_localidad = m.cod_localidad
|
||
AND c.num_matricula = m.num_matricula
|
||
AND c.num_refinanciamiento = m.num_refinanciamiento) AS imp_total_pagado,
|
||
ISNULL((SELECT SUM(c.imp_total - ISNULL(c.imp_dscto, 0))
|
||
FROM sgede_cronograma_matricula c
|
||
WHERE c.cod_localidad = m.cod_localidad
|
||
AND c.num_matricula = m.num_matricula
|
||
AND c.num_refinanciamiento = 1), 0) AS inv_neta,
|
||
m.cod_moneda AS cod_moneda,
|
||
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.cod_localidad_c = m.cod_localidad
|
||
AND cxc.num_matricula = m.num_matricula
|
||
ORDER BY comp.fch_emision DESC), 1) AS imp_tc
|
||
FROM sgeca_matricula m
|
||
INNER JOIN sgema_alumno a ON m.cod_alumno = a.cod_alumno
|
||
WHERE m.num_matricula IN ({placeholders})
|
||
"""
|
||
cursor.execute(sql, mats)
|
||
cols = [c[0] for c in cursor.description]
|
||
filas = [dict(zip(cols, row)) for row in cursor.fetchall()]
|
||
conn.close()
|
||
out = {}
|
||
for f in filas:
|
||
# Si la matrícula está en DÓLARES, convertir montos a soles (× imp_tc).
|
||
_moneda = str(f.get("cod_moneda") or "SOL").strip().upper()
|
||
try: _tc = float(f.get("imp_tc") or 1.0)
|
||
except: _tc = 1.0
|
||
if _moneda != "DOL" or _tc <= 0:
|
||
_tc = 1.0
|
||
_mult = _tc if _moneda == "DOL" else 1.0
|
||
_inv = float(f.get("inv_neta") or 0) * _mult
|
||
_pag = float(f.get("imp_total_pagado") or 0) * _mult
|
||
_mva = float(f.get("monto_venc_ant") or 0) * _mult
|
||
_mvc = float(f.get("monto_venc_cur") or 0) * _mult
|
||
out[str(f.get("num_matricula", "")).strip()] = {
|
||
"dsc_vendedor": str(f.get("dsc_vendedor") or "").strip(),
|
||
"cod_detalle": str(f.get("cod_detalle") or "").strip(),
|
||
"dsc_alumno": str(f.get("dsc_alumno") or "").strip(),
|
||
"num_cuotas_pagadas": int(f.get("num_cuotas_pagadas") or 0),
|
||
"num_cuotas_vencidas": int(f.get("num_cuotas_vencidas") or 0),
|
||
"fch_venc_mas_antigua": str(f.get("fch_venc_mas_antigua") or ""),
|
||
"imp_total_pagado": round(_pag, 2),
|
||
"inv_neta": round(_inv, 2),
|
||
"monto_venc_ant": round(_mva, 2) if _mva > 0 else 0,
|
||
"monto_venc_cur": round(_mvc, 2) if _mvc > 0 else 0,
|
||
}
|
||
return out
|
||
except Exception as e:
|
||
print(f"❌ Error traer_vendedor_cod_detalle: {e}")
|
||
return {}
|
||
|
||
def limpiar_cache_cobranza(self, ano=None):
|
||
"""Vacía el caché anual de cobranza para forzar re-lectura desde SQL.
|
||
Se llama en cada refresco (manual o ciclo de 15 min)."""
|
||
if ano is None:
|
||
self._cache_cronograma.clear()
|
||
self._cache_facturas.clear()
|
||
self._cache_notascredito.clear()
|
||
else:
|
||
self._cache_cronograma.pop(str(ano), None)
|
||
self._cache_facturas.pop(str(ano), None)
|
||
|
||
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
|