From 95e4b39d94614d84e5050435ebc9adbd888832d0 Mon Sep 17 00:00:00 2001 From: Marklogo Date: Wed, 18 Feb 2026 00:49:43 +0100 Subject: [PATCH] Primera version funcional, 'Refactorizacion y control de errores' --- main.py | 225 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 128 insertions(+), 97 deletions(-) diff --git a/main.py b/main.py index 0a0c285..11ecece 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,10 @@ import argparse -import re -import os import json -import warnings +import os +import re import getpass +import tempfile +import sys from datetime import datetime from pypdf import PdfReader, PdfWriter from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter @@ -12,136 +13,166 @@ from pyhanko.stamp import TextStampStyle from pyhanko.pdf_utils.layout import SimpleBoxLayoutRule, AxisAlignment from pyhanko.pdf_utils.text import TextBoxStyle -# Ignorar avisos de pyhanko -warnings.filterwarnings("ignore", category=UserWarning, module="pyhanko") +import logging +logging.getLogger("pyhanko").setLevel(logging.CRITICAL) +logging.getLogger("pyhanko.sign").setLevel(logging.CRITICAL) +logging.getLogger("pyhanko.sign.signers").setLevel(logging.CRITICAL) class SolicitudRRHH: + CONFIG_PATH = "docs/config.json" + CAMPO_PERIODO = "PERÍODO DE TIEMPO POR EL QUE SE SOLICITA" + def __init__(self): self.base_path = os.path.dirname(os.path.abspath(__file__)) self.config = self._cargar_configuracion() - - self.cert_path = os.path.join(self.base_path, self.config["rutas"]["certificado"]) + + self.cert_path = self._ruta(self.config["rutas"]["certificado"]) + self.plantilla_path = self._ruta(self.config["rutas"]["plantilla"]) self.datos_personales = self.config["datos_personales"] - + self.args = self._parsear_argumentos() self.num_dias = self._obtener_num_dias() + def _ruta(self, relativa): + return os.path.join(self.base_path, relativa) + def _cargar_configuracion(self): - config_path = os.path.join(self.base_path, "docs/config.json") + config_path = self._ruta(self.CONFIG_PATH) if not os.path.exists(config_path): - raise FileNotFoundError(f"No se encuentra el archivo de configuración: {config_path}") - with open(config_path, 'r', encoding='utf-8') as f: + raise FileNotFoundError(f"No existe config: {config_path}") + with open(config_path, encoding="utf-8") as f: return json.load(f) def _validar_fecha(self, fecha_str): patron_fecha = r"\d{2}/\d{2}/\d{2}" - patron_completo = rf"^{patron_fecha}((-{patron_fecha})|(,{patron_fecha})*)?$" - if re.match(patron_completo, fecha_str): - return fecha_str - raise argparse.ArgumentTypeError(f"Formato de fecha inválido: {fecha_str}") + patron = rf"^{patron_fecha}((-{patron_fecha})|(,{patron_fecha})*)?$" + if not re.match(patron, fecha_str): + raise argparse.ArgumentTypeError(f"Formato inválido: {fecha_str}") + return fecha_str def _parsear_argumentos(self): - parser = argparse.ArgumentParser(description="Automatización de solicitudes RRHH") - parser.add_argument("tipo", choices=["ap", "vacaciones"], help="Tipo de solicitud") - parser.add_argument("fecha", type=self._validar_fecha, help="Fecha, rango o lista") - parser.add_argument("--dias", type=int, help="Número de días hábiles") + parser = argparse.ArgumentParser(description="Automatización RRHH") + parser.add_argument("tipo", choices=["ap", "vacaciones"],help="Tipo de permiso") + parser.add_argument("fecha", type=self._validar_fecha, help="Fechas en formato dd/mm/yy, se permiten fechas, rangos separados con '-' o listas separados con ','") + parser.add_argument("--dias", type=int) return parser.parse_args() def _obtener_num_dias(self): - if self.args.tipo == "vacaciones": - if self.args.dias: return self.args.dias - while True: - try: - return int(input("👉 Introduce el Nº de días hábiles de vacaciones: ")) - except ValueError: print("❌ Introduce un número entero válido.") - return None + if self.args.tipo != "vacaciones": + return None + + if self.args.dias: + return self.args.dias + + while True: + try: + return int(input("👉 Nº días hábiles: ")) + except ValueError: + print("❌ Número inválido") + + def _formatear_fechas(self): + fechas = self.args.fecha + if "-" in fechas: + inicio, fin = fechas.split("-") + return f"Del {inicio} al {fin}" + if "," in fechas: + return fechas.replace(",", ", ") + return fechas def _obtener_passw_cert(self): - return str(getpass.getpass(prompt="👉 Introduce la contraseña del certificado: ")).encode("utf-8") + return getpass.getpass("👉 Contraseña certificado: ").encode() def _firmar_pdf(self, archivo_entrada, archivo_salida): if not os.path.exists(self.cert_path): - print(f"⚠️ No se encontró el certificado en {self.cert_path}. Se guardará sin firmar.") - os.rename(archivo_entrada, archivo_salida) - return - - with open(archivo_entrada, 'rb') as inf: - w = IncrementalPdfFileWriter(inf) - signer = signers.SimpleSigner.load_pkcs12( - pfx_file=self.cert_path, - passphrase=self._obtener_passw_cert() - ) - - estilo = TextStampStyle( - border_width=0, - text_box_style=TextBoxStyle(font_size=10), - inner_content_layout=SimpleBoxLayoutRule(x_align=AxisAlignment.ALIGN_MIN, y_align=AxisAlignment.ALIGN_MAX), - stamp_text='Firmado por: \n%(signer)s\nFecha: %(ts)s' - ) - - pdf_signer = signers.PdfSigner( - signature_meta=signers.PdfSignatureMetadata(field_name='Firma_Visible'), - signer=signer, - stamp_style=estilo, - new_field_spec=fields.SigFieldSpec( - 'Firma_Visible', - box=(350, 20, 550, 100), - on_page=1 + raise FileNotFoundError("Certificado no encontrado.") + try: + with open(archivo_entrada, "rb") as inf: + writer = IncrementalPdfFileWriter(inf) + signer = signers.SimpleSigner.load_pkcs12( + pfx_file=self.cert_path, + passphrase=self._obtener_passw_cert() ) - ) - with open(archivo_salida, 'wb') as outf: - pdf_signer.sign_pdf(w, output=outf) + estilo = TextStampStyle( + border_width=0, + text_box_style=TextBoxStyle(font_size=10), + inner_content_layout=SimpleBoxLayoutRule( + x_align=AxisAlignment.ALIGN_MIN, + y_align=AxisAlignment.ALIGN_MAX + ), + stamp_text="Firmado por:\n%(signer)s\nFecha: %(ts)s" + ) + + pdf_signer = signers.PdfSigner( + signature_meta=signers.PdfSignatureMetadata( + field_name="Firma_Visible" + ), + signer=signer, + stamp_style=estilo, + new_field_spec=fields.SigFieldSpec( + "Firma_Visible", + box=(350, 20, 550, 100), + on_page=1 + ) + ) + + with open(archivo_salida, "wb") as outf: + pdf_signer.sign_pdf(writer, output=outf) + except Exception as e: + raise ValueError("Contraseña incorrecta o certificado inválido.") from e + + def _generar_campos(self): + campos = {**self.datos_personales} + if self.args.tipo == "ap": + campos["moscoso"] = "/Yes" + else: + campos["diasHabiles"] = "/Yes" + campos["NumDiasHabiles"] = str(self.num_dias) + campos[self.CAMPO_PERIODO] = self._formatear_fechas() + campos["Fecha"] = datetime.now().strftime("%d/%m/%Y") + return campos + + def _generar_nombre_archivo(self): + tipo = self.args.tipo.upper() + fecha = re.split(r"[,-]", self.args.fecha)[0].replace("/", "-") + + return ( + f"{self.datos_personales['Apellido 1']}_" + f"{self.datos_personales['Apellido 2']}_" + f"{self.datos_personales['Nombre']}_" + f"{tipo}_{fecha}.pdf" + ) def generar_pdf(self): - path_plantilla = os.path.join(self.base_path, self.config["rutas"]["plantilla"]) - reader = PdfReader(path_plantilla) + reader = PdfReader(self.plantilla_path) writer = PdfWriter() writer.clone_reader_document_root(reader) - - fechas_raw = self.args.fecha - if "-" in fechas_raw: - f = fechas_raw.split("-") - texto_fechas = f"Del {f[0]} al {f[1]}" - elif "," in fechas_raw: - texto_fechas = f"{fechas_raw.replace(',', ', ')}" - else: - texto_fechas = fechas_raw - - campos_finales = {**self.datos_personales} - - if self.args.tipo == "ap": - campos_finales["moscoso"] = "/Yes" - else: - campos_finales["diasHabiles"] = "/Yes" - campos_finales["NumDiasHabiles"] = str(self.num_dias) - - campos_finales["PERÍODO DE TIEMPO POR EL QUE SE SOLICITA"] = texto_fechas - campos_finales["Fecha"] = datetime.now().strftime("%d/%m/%Y") - + campos = self._generar_campos() for page in writer.pages: - writer.update_page_form_field_values(page, campos_finales) - + writer.update_page_form_field_values(page, campos) + writer.set_need_appearances_writer() nombre_final = self._generar_nombre_archivo() - temp_file = "temp_solicitud.pdf" - - with open(temp_file, "wb") as f: - writer.write(f) - self._firmar_pdf(temp_file, nombre_final) - - if os.path.exists(temp_file): - os.remove(temp_file) - - print(f"✅ Proceso completado con éxito: {nombre_final}") + with tempfile.NamedTemporaryFile(delete=False) as tmp: + writer.write(tmp) + temp_path = tmp.name + + self._firmar_pdf(temp_path, nombre_final) + os.remove(temp_path) + + print(f"✅ PDF generado: {nombre_final}") - def _generar_nombre_archivo(self): - tipo_limpio = self.args.tipo.upper() - primera_fecha = re.split(r'[,-]', self.args.fecha)[0].replace("/", "-") - return f"{self.datos_personales['Apellido 1']}_{self.datos_personales['Apellido 2']}_{self.datos_personales['Nombre']}_{tipo_limpio}_{primera_fecha}.pdf" if __name__ == "__main__": - solicitud = SolicitudRRHH() - solicitud.generar_pdf() + try: + SolicitudRRHH().generar_pdf() + + except (FileNotFoundError, ValueError) as e: + print(f"❌ {e}") + sys.exit(1) + + except Exception as e: + print("💥 Error interno inesperado") + sys.exit(1) \ No newline at end of file