Skip to content

Commit 80b5751

Browse files
refactor(no_conformidades): remove report_registrar shim, drop module-level notification aliases, update tests and cleanup task-manager separation
1 parent d5fa208 commit 80b5751

10 files changed

Lines changed: 196 additions & 589 deletions

src/no_conformidades/no_conformidades_manager.py

Lines changed: 39 additions & 258 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,10 @@
2222
if src_dir not in os.sys.path:
2323
os.sys.path.insert(0, src_dir)
2424

25-
from common.base_task import TareaDiaria
2625
from common.config import config
2726
from common.database import AccessDatabase
2827
from common.html_report_generator import HTMLReportGenerator
2928
from common.user_adapter import get_users_with_fallback
30-
# Las funciones de registro de reportes (enviar_notificacion_calidad / enviar_notificacion_tecnico_individual)
31-
# se han integrado dentro de este manager para simplificar la estructura del módulo.
32-
# Se mantienen wrappers ligeros más abajo para compatibilidad retro con tests que importaban
33-
# no_conformidades.report_registrar.
3429
# --- TypedDicts integrados (antes en types.py) ---
3530
class ARTecnicaRecord(TypedDict, total=False):
3631
CodigoNoConformidad: str
@@ -62,17 +57,29 @@ class ARCalidadProximaRecord(TypedDict, total=False):
6257
AVISO_15_DIAS = "IDCorreo15"
6358

6459

65-
class NoConformidadesManager(TareaDiaria):
66-
"""Manager de No Conformidades usando la nueva arquitectura"""
60+
class NoConformidadesManager:
61+
"""Manager puro de No Conformidades.
6762
68-
def __init__(self):
69-
"""Inicializa el manager incluyendo atributos legacy usados por tests antiguos."""
70-
super().__init__(
71-
name="NoConformidades",
72-
script_filename="run_no_conformidades.py",
73-
task_names=["NCTecnico", "NCCalidad"],
74-
frequency_days=int(os.getenv("NC_FRECUENCIA_DIAS", "1")),
75-
)
63+
Mantiene la lógica de consultas, generación de HTML y registro de correos.
64+
La tarea (NoConformidadesTask) orquesta ejecución y planificación.
65+
"""
66+
67+
def __init__(self, logger: logging.Logger | None = None):
68+
# Nombre esperado por tests de migración
69+
self.name = "NoConformidades"
70+
# Logger independiente (no Task.*) para distinguir en logs
71+
self.logger = logger or logging.getLogger("Manager.NoConformidades")
72+
# Conexión BD tareas (legacy: algunos métodos registran correos directamente)
73+
self.db_tareas = None
74+
try: # Inicialización ligera; si falla se deja en None y métodos manejarán
75+
from common.access_connection_pool import get_tareas_connection_pool
76+
from common.config import Config
77+
cfg = Config()
78+
conn_str = cfg.get_db_tareas_connection_string()
79+
pool = get_tareas_connection_pool(conn_str)
80+
self.db_tareas = AccessDatabase(conn_str, pool=pool)
81+
except Exception as e: # pragma: no cover
82+
self.logger.debug(f"No se pudo inicializar conexión tareas (lazy): {e}")
7683
# Configuración específica
7784
self.dias_alerta_arapc = int(os.getenv("NC_DIAS_ALERTA_ARAPC", "15"))
7885
self.dias_alerta_nc = int(os.getenv("NC_DIAS_ALERTA_NC", "16"))
@@ -108,7 +115,6 @@ def _get_nc_connection(self) -> AccessDatabase:
108115

109116
def _get_tareas_connection(self) -> AccessDatabase:
110117
"""Obtiene la conexión a la base de datos de Tareas"""
111-
# Usar la conexión ya inicializada en BaseTask
112118
return self.db_tareas
113119

114120
def ejecutar_consulta(
@@ -160,12 +166,11 @@ def ejecutar_insercion(self, query: str, params: Optional[tuple] = None) -> bool
160166
return False
161167

162168
def close_connections(self):
163-
"""Cierra conexiones abiertas del manager."""
164-
super().close_connections()
169+
"""Cierra conexiones propias (no cierra db_tareas que puede compartir la Task)."""
165170
if self.db_nc:
166171
try:
167172
self.db_nc.disconnect()
168-
except Exception as e: # Mantener genérica aquí; cierre no crítico
173+
except Exception as e: # pragma: no cover
169174
self.logger.warning(f"Error cerrando conexión NC: {e}")
170175
finally:
171176
self.db_nc = None
@@ -184,7 +189,7 @@ def _register_email_nc(
184189
Devuelve el IDCorreo o None en caso de error.
185190
"""
186191
try:
187-
db = self.db_tareas # Ya abierta en BaseTask
192+
db = self.db_tareas # Conexión inicializada perezosamente en el manager
188193
next_id = db.get_max_id("TbCorreosEnviados", "IDCorreo") + 1
189194
fecha_actual = datetime.now()
190195
insert_query = (
@@ -847,53 +852,7 @@ def get_admin_users(self) -> list[dict[str, Any]]:
847852
self.logger.error(f"Error obteniendo usuarios administradores: {e}")
848853
return []
849854

850-
def should_execute_technical_task(self) -> bool:
851-
try:
852-
from common.utils import should_execute_task
853-
return should_execute_task(
854-
self.db_tareas, "NoConformidadesTecnica", 1, self.logger
855-
)
856-
except Exception as e:
857-
self.logger.error(f"Error verificando si ejecutar tarea técnica: {e}")
858-
return False
859-
860-
def should_execute_quality_task(self) -> bool:
861-
try:
862-
from common.utils import should_execute_weekly_task
863-
return should_execute_weekly_task(
864-
self.db_tareas, "NoConformidadesCalidad", logger=self.logger
865-
)
866-
except Exception as e:
867-
self.logger.error(f"Error verificando si ejecutar tarea de calidad: {e}")
868-
return False
869-
870-
def run(self) -> bool:
871-
try:
872-
self.logger.info("Ejecutando tarea de No Conformidades")
873-
if not self.debe_ejecutarse():
874-
self.logger.info("La tarea de No Conformidades no debe ejecutarse hoy")
875-
return True
876-
success = self.ejecutar_logica_especifica()
877-
if success:
878-
self.marcar_como_completada()
879-
self.logger.info("Tarea de No Conformidades completada exitosamente")
880-
return success
881-
except Exception as e:
882-
self.logger.error(f"Error ejecutando tarea de No Conformidades: {e}")
883-
return False
884-
885-
def ejecutar_logica_especifica(self) -> bool:
886-
try:
887-
self.logger.info("Ejecutando lógica específica de No Conformidades")
888-
self._generar_correo_calidad()
889-
self._generar_correos_tecnicos()
890-
self.logger.info(
891-
"Lógica específica de No Conformidades ejecutada correctamente"
892-
)
893-
return True
894-
except Exception as e:
895-
self.logger.error(f"Error en lógica específica de No Conformidades: {e}")
896-
return False
855+
# Métodos de planificación / ejecución eliminados; responsabilidad de la Task
897856

898857
def _generar_correo_calidad(self):
899858
try:
@@ -918,9 +877,8 @@ def _generar_correo_calidad(self):
918877
)
919878
if html_preview.strip():
920879
self._guardar_html_debug(html_preview, "correo_calidad.html")
921-
# Usar wrapper de módulo para permitir patch en tests
922-
from no_conformidades import no_conformidades_manager as mod
923-
ok = mod.enviar_notificacion_calidad(datos_calidad)
880+
# Llamar directamente al método de instancia (antes usaba shim global)
881+
ok = self.enviar_notificacion_calidad(datos_calidad)
924882
if ok:
925883
self.logger.info(
926884
"Notificación de Calidad registrada",
@@ -1005,15 +963,8 @@ def _generar_correo_tecnico_individual(self, tecnico: str):
1005963
"ars_7_dias": ars_7_dias,
1006964
"ars_vencidas": ars_vencidas,
1007965
}
1008-
# Permitir que tests intercepten la función global enviar_notificacion_tecnico_individual
1009-
try:
1010-
import no_conformidades.no_conformidades_manager as mod # type: ignore
1011-
if hasattr(mod, "enviar_notificacion_tecnico_individual"):
1012-
ok = mod.enviar_notificacion_tecnico_individual(tecnico, datos_tecnico) # type: ignore
1013-
else:
1014-
ok = self.enviar_notificacion_tecnico_individual(tecnico, datos_tecnico)
1015-
except Exception:
1016-
ok = self.enviar_notificacion_tecnico_individual(tecnico, datos_tecnico)
966+
# Llamada directa al método (el shim global ha sido eliminado)
967+
ok = self.enviar_notificacion_tecnico_individual(tecnico, datos_tecnico)
1017968
if ok:
1018969
self.logger.info(
1019970
f"Notificación técnica registrada para {tecnico}",
@@ -1062,184 +1013,14 @@ def _guardar_html_debug(self, html_content: str, filename: str): # pragma: no c
10621013
except Exception as e: # pragma: no cover
10631014
self.logger.error(f"Error guardando HTML debug: {e}")
10641015

1065-
# ---- Shims de compatibilidad legacy para tests ----
1066-
# Algunos tests (test_report_registrar.py y test_no_conformidades_tecnico_registro.py)
1067-
# parchean símbolos a nivel de módulo no_conformidades.no_conformidades_manager
1068-
# como enviar_notificacion_calidad / enviar_notificacion_tecnico_individual en vez de
1069-
# usar métodos de instancia. Conservamos funciones delegadas que crean un manager
1070-
# temporal, de forma análoga a los wrappers en report_registrar.
1071-
1072-
def enviar_notificacion_calidad(datos_calidad: dict[str, Any]) -> bool: # pragma: no cover - compatibilidad tests
1073-
mgr = NoConformidadesManager()
1074-
try:
1075-
return mgr.enviar_notificacion_calidad(datos_calidad)
1076-
finally:
1077-
mgr.close_connections()
1078-
1079-
1080-
def enviar_notificacion_tecnico_individual(tecnico: str, datos_tecnico: dict[str, Any]) -> bool: # pragma: no cover
1081-
mgr = NoConformidadesManager()
1082-
try:
1083-
# Delegar al método de instancia; _obtener_email_tecnico puede parchearse vía shim separado
1084-
return mgr.enviar_notificacion_tecnico_individual(tecnico, datos_tecnico)
1085-
finally:
1086-
mgr.close_connections()
1087-
1088-
1089-
def _register_email_nc(**kwargs) -> Optional[int]: # pragma: no cover - shim para tests
1090-
# Intento directo usando AccessDatabase (permite patch en tests)
1091-
try:
1092-
db = AccessDatabase("dummy") # En tests será objeto fake con get_max_id
1093-
next_id = db.get_max_id("TbCorreosEnviados", "IDCorreo") + 1
1094-
fecha_actual = datetime.now()
1095-
insert_query = (
1096-
"INSERT INTO TbCorreosEnviados (IDCorreo, Aplicacion, Asunto, Cuerpo, Destinatarios, DestinatariosConCopia, DestinatariosConCopiaOculta, FechaGrabacion) "
1097-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
1098-
)
1099-
with db.get_connection() as conn:
1100-
cur = conn.cursor()
1101-
cur.execute(
1102-
insert_query,
1103-
[
1104-
next_id,
1105-
kwargs["application"],
1106-
kwargs["subject"],
1107-
kwargs["body"].strip(),
1108-
kwargs["recipients"],
1109-
kwargs.get("admin_emails", ""),
1110-
"",
1111-
fecha_actual,
1112-
],
1113-
)
1114-
conn.commit()
1115-
return next_id
1116-
except Exception:
1117-
mgr = NoConformidadesManager()
1118-
try:
1119-
return mgr._register_email_nc(**kwargs)
1120-
finally:
1121-
mgr.close_connections()
1122-
1123-
1124-
def _register_arapc_notification(*args) -> bool: # pragma: no cover - shim para tests
1125-
try:
1126-
db = AccessDatabase("dummy")
1127-
with db.get_connection() as conn:
1128-
cur = conn.cursor()
1129-
1130-
def get_next_id():
1131-
try:
1132-
cur.execute("SELECT Max(TbNCARAvisos.ID) AS Maximo FROM TbNCARAvisos")
1133-
r = cur.fetchone()
1134-
return (r[0] + 1) if r and r[0] is not None else 1
1135-
except Exception:
1136-
return 1
1137-
1138-
id_correo, arapcs_15, arapcs_7, arapcs_0 = args[0], args[1], args[2], args[3]
1139-
for acc in arapcs_15:
1140-
cur.execute(
1141-
"INSERT INTO TbNCARAvisos (ID, IDAR, IDCorreo15, Fecha) VALUES (?, ?, ?, ?)",
1142-
[get_next_id(), acc, id_correo, datetime.now()],
1143-
)
1144-
for acc in arapcs_7:
1145-
cur.execute(
1146-
"INSERT INTO TbNCARAvisos (ID, IDAR, IDCorreo7, Fecha) VALUES (?, ?, ?, ?)",
1147-
[get_next_id(), acc, id_correo, datetime.now()],
1148-
)
1149-
for acc in arapcs_0:
1150-
cur.execute(
1151-
"INSERT INTO TbNCARAvisos (ID, IDAR, IDCorreo0, Fecha) VALUES (?, ?, ?, ?)",
1152-
[get_next_id(), acc, id_correo, datetime.now()],
1153-
)
1154-
conn.commit()
1155-
return True
1156-
except Exception:
1157-
mgr = NoConformidadesManager()
1016+
def ejecutar_logica_especifica(self) -> bool:
1017+
"""Ejecuta la lógica principal (antes en Task / método heredado)."""
11581018
try:
1159-
return mgr._register_arapc_notification(*args)
1160-
finally:
1161-
mgr.close_connections()
1162-
1163-
1164-
def _obtener_email_tecnico(tecnico: str) -> Optional[str]: # pragma: no cover - shim tests
1165-
mgr = NoConformidadesManager()
1166-
try:
1167-
return mgr._obtener_email_tecnico(tecnico)
1168-
finally:
1169-
mgr.close_connections()
1170-
1171-
def main(): # pragma: no cover (entry point manual)
1172-
"""
1173-
Función principal para ejecutar el manager directamente con argumentos
1174-
"""
1175-
import argparse
1176-
import sys
1177-
1178-
# Logging global ya debe estar configurado por el proceso maestro.
1179-
logger = logging.getLogger(__name__)
1180-
1181-
# Configurar argumentos
1182-
parser = argparse.ArgumentParser(description="Manager de No Conformidades")
1183-
parser.add_argument(
1184-
"--force-calidad",
1185-
action="store_true",
1186-
help="Forzar generación del correo de calidad",
1187-
)
1188-
parser.add_argument(
1189-
"--force-tecnicos",
1190-
action="store_true",
1191-
help="Forzar generación de correos de técnicos",
1192-
)
1193-
parser.add_argument("--debug", action="store_true", help="Activar modo debug")
1194-
1195-
args = parser.parse_args()
1196-
if args.debug:
1197-
logger.setLevel(logging.DEBUG)
1198-
1199-
# Configurar nivel de logging si debug está activado
1200-
if args.debug:
1201-
logging.getLogger().setLevel(logging.DEBUG)
1202-
1203-
manager = None
1204-
try:
1205-
logger.info("=== INICIANDO MANAGER NO CONFORMIDADES ===")
1206-
1207-
# Crear el manager
1208-
manager = NoConformidadesManager()
1209-
1210-
if args.force_calidad:
1211-
logger.info("Ejecutando generación forzada del correo de calidad...")
1212-
manager._generar_correo_calidad()
1213-
1214-
if args.force_tecnicos:
1215-
logger.info("Ejecutando generación forzada de correos de técnicos...")
1216-
manager._generar_correos_tecnicos()
1217-
1218-
if not args.force_calidad and not args.force_tecnicos:
1219-
logger.info("Ejecutando lógica completa...")
1220-
success = manager.ejecutar_logica_especifica()
1221-
if not success:
1222-
logger.error("Error en la ejecución de la lógica específica")
1223-
return 1
1224-
1225-
logger.info("=== MANAGER NO CONFORMIDADES COMPLETADO EXITOSAMENTE ===")
1226-
return 0
1227-
1228-
except Exception as e:
1229-
logger.error(f"Error crítico en el manager: {e}")
1230-
return 1
1231-
finally:
1232-
# Cerrar conexiones
1233-
if manager:
1234-
try:
1235-
manager.close_connections()
1236-
logger.info("Conexiones cerradas correctamente")
1237-
except Exception as e:
1238-
logger.warning(f"Error cerrando conexiones: {e}")
1239-
1240-
1241-
if __name__ == "__main__":
1242-
import sys
1019+
self._generar_correo_calidad()
1020+
self._generar_correos_tecnicos()
1021+
return True
1022+
except Exception as e: # pragma: no cover
1023+
self.logger.error(f"Error en ejecutar_logica_especifica: {e}")
1024+
return False
12431025

1244-
exit_code = main()
1245-
sys.exit(exit_code)
1026+
# (Eliminados alias module-level de compatibilidad; los tests ahora usan la instancia directamente.)

0 commit comments

Comments
 (0)