"""Génération d'avis de sanction à partir du template AcroForm officiel. Le template est `data/templates/GF_FO_Avis_de_sanction.pdf`. Il contient 9 champs de formulaire qu'on remplit programmatiquement avec pypdf, sans aplatir (les champs restent éditables après téléchargement). """ from __future__ import annotations import io import json import os from datetime import date from pathlib import Path from typing import Optional import pypdf from sqlalchemy.orm import Session from src.db import Apprenti, ApprentiFiche _ROOT = Path(__file__).resolve().parent.parent _DATA_DIR = Path(os.getenv("DATA_DIR", str(_ROOT / "data"))) _TEMPLATE_PATH = _DATA_DIR / "templates" / "GF_FO_Avis_de_sanction.pdf" _SETTINGS_PATH = _DATA_DIR / "settings.json" # Mêmes valeurs par défaut que la page Paramètres (pages/params.py). _DEFAULT_TEXTE_SANCTION = ( "Selon le règlement de l'EM, l'apprenti a dépassé le nombre d'absences limite." ) _DEFAULT_CHEF_SECTION = "Patrick Rausis" def _load_settings() -> dict: if _SETTINGS_PATH.exists(): try: return json.loads(_SETTINGS_PATH.read_text(encoding="utf-8")) except Exception: return {} return {} def generate_avis_pdf( sess: Session, apprenti_id: int, prof_name: str = "", ) -> Optional[bytes]: """Renvoie les bytes d'un PDF d'avis de sanction pré-rempli pour l'apprenti. Champs remplis depuis ApprentiFiche.entreprise_* (adresse, NPA-Ville et NomParents = nom entreprise) puisque les parents ne sont pas stockés. Texte de description et chef de section depuis data/settings.json. Renvoie None si le template est introuvable ou l'apprenti n'existe pas. """ if not _TEMPLATE_PATH.exists(): return None apprenti = sess.get(Apprenti, apprenti_id) if apprenti is None: return None fiche: Optional[ApprentiFiche] = apprenti.fiche settings = _load_settings() # Construction des valeurs npa_ville = "" if fiche: cp = (fiche.entreprise_code_postal or "").strip() loc = (fiche.entreprise_localite or "").strip() npa_ville = f"{cp} {loc}".strip() field_values: dict[str, str] = { "NomApprenti": f"{apprenti.prenom} {apprenti.nom}".strip(), "Classe": apprenti.classe or "", "NomParents": (fiche.entreprise_nom if fiche else "") or "", "Adresse": (fiche.entreprise_adresse if fiche else "") or "", "NPA-Ville": npa_ville, "Date": date.today().strftime("%d.%m.%Y"), "TexteDescription": settings.get("texte_sanction") or _DEFAULT_TEXTE_SANCTION, "Prof": prof_name or "", "CS": settings.get("chef_section") or _DEFAULT_CHEF_SECTION, } # Lecture du template + clone vers writer (préserve la structure AcroForm) reader = pypdf.PdfReader(str(_TEMPLATE_PATH)) writer = pypdf.PdfWriter(clone_from=reader) # Remplissage des champs sur chaque page (AcroForm peut être réparti). # auto_regenerate=False : conserve les valeurs même si Reader recalcule # les apparences (Acrobat les redessine à l'ouverture). for page in writer.pages: try: writer.update_page_form_field_values( page, field_values, auto_regenerate=False ) except Exception: # Champ peut-être absent de cette page : ignore et continue pass # Force les champs comme NeedAppearances pour que les viewers redessinent # correctement les valeurs à l'ouverture. try: if "/AcroForm" in writer._root_object: writer._root_object["/AcroForm"].update( {pypdf.generic.NameObject("/NeedAppearances"): pypdf.generic.BooleanObject(True)} ) except Exception: pass buf = io.BytesIO() writer.write(buf) return buf.getvalue()