commit 4ba9bc686933567b834c8adf4447225a2e48232f Author: Marklogo Date: Tue Feb 17 21:06:48 2026 +0100 Primera version funcional diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94824b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +*.pyo +*.pdf +.env +.venv \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/certificado/.gitkeep b/certificado/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/certificado/certificado.p12 b/certificado/certificado.p12 new file mode 100644 index 0000000..0520c7f Binary files /dev/null and b/certificado/certificado.p12 differ diff --git a/docs/campos b/docs/campos new file mode 100644 index 0000000..9483403 --- /dev/null +++ b/docs/campos @@ -0,0 +1,63 @@ +DNI +Nombre +Apellido 1 +Apellido 2 +Cuerpo o escala +Área +Primergrado +segundoGrado +ausencia1hJornada +ausencia1hFraccionada +reduccion1h +reducciónMediaHora +horasSindicales +diasSindicales +diasHabiles +PermisoParto +permisoadopcion +PermisoPaternidad +fallecimiento +trasladoSin +trasladoCon +examenesFinales +examenesPrenatales +Lactancia +NacimientoPrematuro +indispensable +moscoso +matrimonio +sindicales +CursoOrganismo +CursoAGE +CursoSindicatos +CursoPracticas +TomaProvision +TomaNuevoCuerpo +jubilacion +InteresParticular +FamiliarGrave +LicenciaEstudio +LicenciaPropios +LicenciaDesarrollo +TotalEnfermedadSin +TotalEnfermedadCon +ParcialEnfermedad +GestiónOficial +GestiónPersonal +ConisionServicio +viajeOficial +PorNocturnas +FueraHorario +refuerzos +noBeneficios +curso +decision +NumDiasHabiles +nmHorasSidicales +Otros +PERÍODO DE TIEMPO POR EL QUE SE SOLICITA +Imprimir +OposicionesInterna +numDiasSindicales +Fecha +guarda \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..9782e90 --- /dev/null +++ b/main.py @@ -0,0 +1,138 @@ +import argparse +import re +import os +import warnings +warnings.filterwarnings("ignore", category=UserWarning, module="pyhanko") +import getpass +from pypdf import PdfReader, PdfWriter +from datetime import datetime +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 + +class SolicitudRRHH: + def __init__(self): + self.cert_path = os.path.join(os.path.dirname(__file__), "certificado", "certificado.p12") + self.datos_personales = { + "DNI": "33340763D", + "Nombre": "MARCOS", + "Apellido 1": "LOPEZ", + "Apellido 2": "GOMEZ", + "Cuerpo o escala": "C.TEC. AUX. DE INFORMATICA ADMON. ESTADO" + } + self.args = self._parsear_argumentos() + self.num_dias = self._obtener_num_dias() + + 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}") + + 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") + 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 + + def _obtener_passw_cert(self): + return str(getpass.getpass(prompt="👉 Introduce la contraseña del certificado: ")).encode("utf-8") + + def _firmar_pdf(self, archivo_entrada, archivo_salida): + if not os.path.exists(self.cert_path): + print(f"⚠️ No se encontró el certificado.") + 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 + ) + ) + + with open(archivo_salida, 'wb') as outf: + pdf_signer.sign_pdf(w, output=outf) + + def generar_pdf(self, path_plantilla="./docs/Plantilla.pdf"): + reader = PdfReader(path_plantilla) + 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") + + for page in writer.pages: + writer.update_page_form_field_values(page, campos_finales) + + writer.set_need_appearances_writer() + + # --- FLUJO DE GUARDADO Y FIRMA --- + 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}") + + 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() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..422ac41 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,19 @@ +asn1crypto==1.5.1 +certifi==2026.1.4 +cffi==2.0.0 +charset-normalizer==3.4.4 +cryptography==46.0.5 +fonttools==4.61.1 +idna==3.11 +lxml==6.0.2 +oscrypto==1.3.0 +pycparser==3.0 +pyHanko==0.33.0 +pyhanko-certvalidator==0.29.1 +pypdf==6.7.0 +PyYAML==6.0.3 +requests==2.32.5 +tzlocal==5.3.1 +uharfbuzz==0.53.3 +uritools==6.0.1 +urllib3==2.6.3