Primera version funcional, 'Refactorizacion y control de errores'

This commit is contained in:
Marklogo 2026-02-18 00:49:43 +01:00
parent 0ec8041ac7
commit 95e4b39d94

225
main.py
View File

@ -1,9 +1,10 @@
import argparse import argparse
import re
import os
import json import json
import warnings import os
import re
import getpass import getpass
import tempfile
import sys
from datetime import datetime from datetime import datetime
from pypdf import PdfReader, PdfWriter from pypdf import PdfReader, PdfWriter
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter 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.layout import SimpleBoxLayoutRule, AxisAlignment
from pyhanko.pdf_utils.text import TextBoxStyle from pyhanko.pdf_utils.text import TextBoxStyle
# Ignorar avisos de pyhanko import logging
warnings.filterwarnings("ignore", category=UserWarning, module="pyhanko") logging.getLogger("pyhanko").setLevel(logging.CRITICAL)
logging.getLogger("pyhanko.sign").setLevel(logging.CRITICAL)
logging.getLogger("pyhanko.sign.signers").setLevel(logging.CRITICAL)
class SolicitudRRHH: class SolicitudRRHH:
CONFIG_PATH = "docs/config.json"
CAMPO_PERIODO = "PERÍODO DE TIEMPO POR EL QUE SE SOLICITA"
def __init__(self): def __init__(self):
self.base_path = os.path.dirname(os.path.abspath(__file__)) self.base_path = os.path.dirname(os.path.abspath(__file__))
self.config = self._cargar_configuracion() 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.datos_personales = self.config["datos_personales"]
self.args = self._parsear_argumentos() self.args = self._parsear_argumentos()
self.num_dias = self._obtener_num_dias() self.num_dias = self._obtener_num_dias()
def _ruta(self, relativa):
return os.path.join(self.base_path, relativa)
def _cargar_configuracion(self): 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): if not os.path.exists(config_path):
raise FileNotFoundError(f"No se encuentra el archivo de configuración: {config_path}") raise FileNotFoundError(f"No existe config: {config_path}")
with open(config_path, 'r', encoding='utf-8') as f: with open(config_path, encoding="utf-8") as f:
return json.load(f) return json.load(f)
def _validar_fecha(self, fecha_str): def _validar_fecha(self, fecha_str):
patron_fecha = r"\d{2}/\d{2}/\d{2}" patron_fecha = r"\d{2}/\d{2}/\d{2}"
patron_completo = rf"^{patron_fecha}((-{patron_fecha})|(,{patron_fecha})*)?$" patron = rf"^{patron_fecha}((-{patron_fecha})|(,{patron_fecha})*)?$"
if re.match(patron_completo, fecha_str): if not re.match(patron, fecha_str):
return fecha_str raise argparse.ArgumentTypeError(f"Formato inválido: {fecha_str}")
raise argparse.ArgumentTypeError(f"Formato de fecha inválido: {fecha_str}") return fecha_str
def _parsear_argumentos(self): def _parsear_argumentos(self):
parser = argparse.ArgumentParser(description="Automatización de solicitudes RRHH") parser = argparse.ArgumentParser(description="Automatización RRHH")
parser.add_argument("tipo", choices=["ap", "vacaciones"], help="Tipo de solicitud") parser.add_argument("tipo", choices=["ap", "vacaciones"],help="Tipo de permiso")
parser.add_argument("fecha", type=self._validar_fecha, help="Fecha, rango o lista") 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, help="Número de días hábiles") parser.add_argument("--dias", type=int)
return parser.parse_args() return parser.parse_args()
def _obtener_num_dias(self): def _obtener_num_dias(self):
if self.args.tipo == "vacaciones": if self.args.tipo != "vacaciones":
if self.args.dias: return self.args.dias return None
while True:
try: if self.args.dias:
return int(input("👉 Introduce el Nº de días hábiles de vacaciones: ")) return self.args.dias
except ValueError: print("❌ Introduce un número entero válido.")
return None 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): 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): def _firmar_pdf(self, archivo_entrada, archivo_salida):
if not os.path.exists(self.cert_path): if not os.path.exists(self.cert_path):
print(f"⚠️ No se encontró el certificado en {self.cert_path}. Se guardará sin firmar.") raise FileNotFoundError("Certificado no encontrado.")
os.rename(archivo_entrada, archivo_salida) try:
return with open(archivo_entrada, "rb") as inf:
writer = IncrementalPdfFileWriter(inf)
with open(archivo_entrada, 'rb') as inf: signer = signers.SimpleSigner.load_pkcs12(
w = IncrementalPdfFileWriter(inf) pfx_file=self.cert_path,
signer = signers.SimpleSigner.load_pkcs12( passphrase=self._obtener_passw_cert()
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: estilo = TextStampStyle(
pdf_signer.sign_pdf(w, output=outf) 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): def generar_pdf(self):
path_plantilla = os.path.join(self.base_path, self.config["rutas"]["plantilla"]) reader = PdfReader(self.plantilla_path)
reader = PdfReader(path_plantilla)
writer = PdfWriter() writer = PdfWriter()
writer.clone_reader_document_root(reader) writer.clone_reader_document_root(reader)
campos = self._generar_campos()
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")
for page in writer.pages: 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() writer.set_need_appearances_writer()
nombre_final = self._generar_nombre_archivo() 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) with tempfile.NamedTemporaryFile(delete=False) as tmp:
writer.write(tmp)
if os.path.exists(temp_file): temp_path = tmp.name
os.remove(temp_file)
self._firmar_pdf(temp_path, nombre_final)
print(f"✅ Proceso completado con éxito: {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__": if __name__ == "__main__":
solicitud = SolicitudRRHH() try:
solicitud.generar_pdf() 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)