2222if src_dir not in os .sys .path :
2323 os .sys .path .insert (0 , src_dir )
2424
25- from common .base_task import TareaDiaria
2625from common .config import config
2726from common .database import AccessDatabase
2827from common .html_report_generator import HTMLReportGenerator
2928from 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) ---
3530class ARTecnicaRecord (TypedDict , total = False ):
3631 CodigoNoConformidad : str
@@ -62,17 +57,29 @@ class ARCalidadProximaRecord(TypedDict, total=False):
6257AVISO_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