eptm_dashboard/src/sanction_pdf.py

126 lines
4.2 KiB
Python

"""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 = "",
texte_override: Optional[str] = None,
chef_override: Optional[str] = None,
) -> 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.
Si `texte_override` ou `chef_override` est fourni (non vide), il remplace
la valeur issue des paramètres.
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": (
(texte_override or "").strip()
or settings.get("texte_sanction")
or _DEFAULT_TEXTE_SANCTION
),
"Prof": prof_name or "",
"CS": (
(chef_override or "").strip()
or 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()