import argparse import json 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 from pyhanko.sign import signers, fields from pyhanko.stamp import TextStampStyle from pyhanko.pdf_utils.layout import SimpleBoxLayoutRule, AxisAlignment from pyhanko.pdf_utils.text import TextBoxStyle 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 = 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 = self._ruta(self.CONFIG_PATH) if not os.path.exists(config_path): 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 = 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 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": 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 getpass.getpass("👉 Contraseña certificado: ").encode() def _firmar_pdf(self, archivo_entrada, archivo_salida): if not os.path.exists(self.cert_path): 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() ) 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): reader = PdfReader(self.plantilla_path) writer = PdfWriter() writer.clone_reader_document_root(reader) campos = self._generar_campos() for page in writer.pages: writer.update_page_form_field_values(page, campos) writer.set_need_appearances_writer() nombre_final = self._generar_nombre_archivo() 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}") if __name__ == "__main__": 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)