Actualizar backend/core/data_manager.py

This commit is contained in:
2026-09-22 11:04:13 -05:00
parent acf82f1f8f
commit 9792b07d5a

View File

@@ -58,8 +58,10 @@ class DataManager:
self.query_cronograma_sql = "" self.query_cronograma_sql = ""
self.query_facturas_sql = "" self.query_facturas_sql = ""
# Caché en memoria de las consultas ANUALES de cobranza (evita re-ejecutar # Caché en memoria de las consultas ANUALES de cobranza. Evita re-ejecutar
# la consulta del año completo en cada combinación de filtros). # 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_cronograma = {} # str(ano) -> filas
self._cache_facturas = {} # str(ano) -> filas self._cache_facturas = {} # str(ano) -> filas
@@ -99,28 +101,20 @@ class DataManager:
self.pendientes_data = set() self.pendientes_data = set()
def _get_json(self, url, default=None): def _get_json(self, url, default=None):
# 3 intentos, timeout 15s. Si falla, avisa en consola y devuelve el default. try:
for intento in range(1, 4): r = requests.get(url, timeout=4)
try: r.raise_for_status()
r = requests.get(url, timeout=15) return r.json()
r.raise_for_status() except:
return r.json() return default or {}
except Exception as e:
print(f"⚠️ [GitHub] intento {intento}/3 fallo JSON {url}: {e}")
print(f"❌ [GitHub] NO se pudo descargar (JSON): {url}")
return default or {}
def _get_text(self, url): def _get_text(self, url):
# 3 intentos, timeout 15s. Si falla, avisa en consola y devuelve texto vacio. try:
for intento in range(1, 4): r = requests.get(url, timeout=4)
try: r.raise_for_status()
r = requests.get(url, timeout=15) return r.text
r.raise_for_status() except:
return r.text return ""
except Exception as e:
print(f"⚠️ [GitHub] intento {intento}/3 fallo TEXT {url}: {e}")
print(f"❌ [GitHub] NO se pudo descargar (TEXT): {url}")
return ""
def _cargar_pendientes(self): def _cargar_pendientes(self):
# Ya cargado en paralelo dentro de cargar_toda_configuracion # Ya cargado en paralelo dentro de cargar_toda_configuracion
@@ -135,7 +129,7 @@ class DataManager:
def get_connection(self): def get_connection(self):
import pyodbc import pyodbc
conn_str = ( conn_str = (
f"DRIVER={{ODBC Driver 17 for SQL Server}};" # <--- ¡ESTE ES EL CAMBIO MÁGICO! f"DRIVER={{SQL Server}};"
f"SERVER={self.server};" f"SERVER={self.server};"
f"DATABASE={self.database};" f"DATABASE={self.database};"
f"UID={self.username};" f"UID={self.username};"
@@ -637,7 +631,8 @@ class DataManager:
ventas_por_vendedor[vendedor]['monto_pc'] += inv_neta ventas_por_vendedor[vendedor]['monto_pc'] += inv_neta
ventas_por_vendedor[vendedor]['cantidad_pc'] += 1 ventas_por_vendedor[vendedor]['cantidad_pc'] += 1
# COLUMNA 4 y 5 — MES PASADO: matrícula anterior + pagó 1° cuota en el mes del filtro # 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: if matricula_int in set_lista_pendientes and pago_en_fecha_correcta:
ventas_por_vendedor[vendedor]['monto_pendientes'] += inv_neta ventas_por_vendedor[vendedor]['monto_pendientes'] += inv_neta
ventas_por_vendedor[vendedor]['cantidad_pendientes'] += 1 ventas_por_vendedor[vendedor]['cantidad_pendientes'] += 1
@@ -680,68 +675,6 @@ class DataManager:
print(f"❌ Error cronograma cobranza: {e}") print(f"❌ Error cronograma cobranza: {e}")
return [] return []
def traer_vendedor_cod_detalle(self, matriculas):
"""Para el apartado RETIROS: por cada num_matricula (solo las POR RETIRAR),
trae dsc_vendedor, cod_detalle, dsc_alumno, N° cuotas pagadas/vencidas,
fecha de vencimiento más antigua, imp_total_pagado e inv_neta. Consulta ligera."""
try:
mats = [str(m).strip() for m in (matriculas or []) if str(m).strip()]
if not mats:
return {}
placeholders = ",".join(["?"] * len(mats))
query = 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 < CAST(GETDATE() AS DATE)) AS num_cuotas_vencidas,
(SELECT MIN(c.fch_vencimiento) 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 < CAST(GETDATE() AS DATE)) AS fch_venc_mas_antigua,
(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
FROM sgeca_matricula m
INNER JOIN sgema_alumno a ON m.cod_alumno = a.cod_alumno
WHERE m.num_matricula IN ({placeholders})
"""
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute(query, mats)
out = {}
for row in cursor.fetchall():
mat = str(row[0]).strip()
fvenc = row[6]
out[mat] = {
"dsc_vendedor": (row[1] or "").strip() if row[1] else "",
"cod_detalle": row[2] if row[2] is not None else "",
"dsc_alumno": (row[3] or "").strip() if row[3] else "",
"num_cuotas_pagadas": int(row[4] or 0),
"num_cuotas_vencidas": int(row[5] or 0),
"fch_venc_mas_antigua": (str(fvenc)[:10] if fvenc else ""),
"imp_total_pagado": float(row[7] or 0),
"inv_neta": float(row[8] or 0),
}
conn.close()
return out
except Exception as e:
print(f"❌ Error traer_vendedor_cod_detalle: {e}")
return {}
def ejecutar_consulta_facturas_cobranza(self, ano): def ejecutar_consulta_facturas_cobranza(self, ano):
clave = str(ano) clave = str(ano)
en_cache = self._cache_facturas.get(clave) en_cache = self._cache_facturas.get(clave)
@@ -762,6 +695,84 @@ class DataManager:
print(f"❌ Error facturas cobranza: {e}") print(f"❌ Error facturas cobranza: {e}")
return [] return []
def traer_vendedor_cod_detalle(self, matriculas):
"""Dada una lista de num_matricula (POR RETIRAR, pocas), devuelve por cada una:
{num_matricula, dsc_vendedor, cod_detalle}. Consulta directa y ligera
(filtra solo esas matrículas). NO toca cobranza ni sus cachés."""
mats = [str(m).strip() for m in (matriculas or []) if str(m).strip()]
if not mats:
return {}
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 < CAST(GETDATE() AS DATE)) AS num_cuotas_vencidas,
(SELECT MIN(c.fch_vencimiento)
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 < CAST(GETDATE() AS DATE)) AS fch_venc_mas_antigua,
(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
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:
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 "")[:10],
"imp_total_pagado": float(f.get("imp_total_pagado") or 0),
"inv_neta": float(f.get("inv_neta") or 0),
}
return out
except Exception as e:
print(f"❌ Error traer_vendedor_cod_detalle: {e}")
return {}
def limpiar_cache_cobranza(self, ano=None): def limpiar_cache_cobranza(self, ano=None):
"""Vacía el caché anual de cobranza para forzar re-lectura desde SQL. """Vacía el caché anual de cobranza para forzar re-lectura desde SQL.
Se llama en cada refresco (manual o ciclo de 15 min).""" Se llama en cada refresco (manual o ciclo de 15 min)."""