diff --git a/backend/core/data_manager.py b/backend/core/data_manager.py index ee961bb..fd486d0 100644 --- a/backend/core/data_manager.py +++ b/backend/core/data_manager.py @@ -43,6 +43,7 @@ class DataManager: 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 = {} @@ -57,6 +58,7 @@ class DataManager: 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/ @@ -64,6 +66,7 @@ class DataManager: # 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() @@ -87,6 +90,7 @@ class DataManager: "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()} @@ -129,7 +133,7 @@ class DataManager: def get_connection(self): import pyodbc conn_str = ( - f"DRIVER={{ODBC Driver 17 for SQL Server}};" + f"DRIVER={{SQL Server}};" f"SERVER={self.server};" f"DATABASE={self.database};" f"UID={self.username};" @@ -699,13 +703,37 @@ class DataManager: 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 directa y ligera - (filtra solo esas matrículas). NO toca cobranza ni sus cachés.""" + {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: @@ -777,20 +805,20 @@ class DataManager: 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 + 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}) @@ -801,13 +829,15 @@ class DataManager: 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 + if _moneda != "DOL" or _tc <= 0: + _tc = 1.0 _mult = _tc if _moneda == "DOL" else 1.0 - _pag = float(f.get("imp_total_pagado") or 0) * _mult _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()] = { @@ -833,6 +863,7 @@ class DataManager: 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)