diff --git a/backend/core/data_manager.py b/backend/core/data_manager.py index 058b5d9..d517908 100644 --- a/backend/core/data_manager.py +++ b/backend/core/data_manager.py @@ -58,8 +58,10 @@ class DataManager: self.query_cronograma_sql = "" self.query_facturas_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). + # 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 @@ -99,28 +101,20 @@ class DataManager: self.pendientes_data = set() def _get_json(self, url, default=None): - # 3 intentos, timeout 15s. Si falla, avisa en consola y devuelve el default. - for intento in range(1, 4): - try: - r = requests.get(url, timeout=15) - r.raise_for_status() - return r.json() - 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 {} + try: + r = requests.get(url, timeout=4) + r.raise_for_status() + return r.json() + except: + return default or {} def _get_text(self, url): - # 3 intentos, timeout 15s. Si falla, avisa en consola y devuelve texto vacio. - for intento in range(1, 4): - try: - r = requests.get(url, timeout=15) - r.raise_for_status() - return r.text - except Exception as e: - print(f"⚠️ [GitHub] intento {intento}/3 fallo TEXT {url}: {e}") - print(f"❌ [GitHub] NO se pudo descargar (TEXT): {url}") - return "" + 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 @@ -135,7 +129,7 @@ class DataManager: def get_connection(self): import pyodbc 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"DATABASE={self.database};" f"UID={self.username};" @@ -637,7 +631,8 @@ class DataManager: 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 + # 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 @@ -680,68 +675,6 @@ class DataManager: print(f"❌ Error cronograma cobranza: {e}") 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): clave = str(ano) en_cache = self._cache_facturas.get(clave) @@ -762,6 +695,84 @@ class DataManager: print(f"❌ Error facturas cobranza: {e}") 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): """Vacía el caché anual de cobranza para forzar re-lectura desde SQL. Se llama en cada refresco (manual o ciclo de 15 min)."""