126 lines
4.2 KiB
Python
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()
|