"""Génération d'avis de retenue à partir du template AcroForm. Template : `data/templates/GF_FO_Avis_de_retenue.pdf`. Le champ `Date` du template a 3 widgets-enfants partagés (un par ligne du formulaire). On les sépare en 3 champs distincts (`Date_devoir`, `Date_comportement`, `Date_retard`) puis on remplit uniquement celui correspondant à la case cochée. Le PDF généré reste éditable (formulaire préservé). """ from __future__ import annotations import io import os from datetime import date as _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_retenue.pdf" _MOIS_FR = [ "janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre", ] # Mapping case → suffixe + index (ordre des widgets enfants triés par Y desc) _CASE_TO_SUFFIX = { "devoir": ("Date_devoir", 0), "comportement": ("Date_comportement", 1), "retard": ("Date_retard", 2), } def format_date_long(d: _date) -> str: """Formate une date en 'jour mois année' (ex: '12 mars 2026').""" return f"{d.day} {_MOIS_FR[d.month - 1]} {d.year}" def generate_retenue_pdf( sess: Session, apprenti_id: int, *, profession: str, retenue_date: _date, probleme_date: _date, case: str, # "devoir" | "comportement" | "retard" branche: str = "", remarque: str = "", prof_name: str = "", ) -> Optional[bytes]: """Pré-remplit le template puis aplatit le PDF. Renvoie les bytes du PDF aplati.""" if not _TEMPLATE_PATH.exists(): return None apprenti = sess.get(Apprenti, apprenti_id) if apprenti is None: return None fiche: Optional[ApprentiFiche] = apprenti.fiche classe_full = ( f"{profession.strip()} {apprenti.classe}".strip() if profession else apprenti.classe ) npa_ville = "" if fiche: cp = (fiche.entreprise_code_postal or "").strip() loc = (fiche.entreprise_localite or "").strip() npa_ville = f"{cp} {loc}".strip() # 1. Lecture template + clone reader = pypdf.PdfReader(str(_TEMPLATE_PATH)) writer = pypdf.PdfWriter(clone_from=reader) # 2. Séparer les 3 widgets du champ Date en 3 champs distincts. # Après cette opération, on peut remplir chaque Date_xxx individuellement. _split_date_field(writer) # 3. Remplit les champs texte (Date_xxx inclus pour la case sélectionnée) target_date_field = _CASE_TO_SUFFIX.get(case, (None, None))[0] field_values: dict[str, str] = { "NomApprenti": f"{apprenti.prenom} {apprenti.nom}".strip(), "Classe": classe_full, "NomEntreprise": (fiche.entreprise_nom if fiche else "") or "", "Adresse": (fiche.entreprise_adresse if fiche else "") or "", "NPA-Ville": npa_ville, "RetenueDateHeure": retenue_date.strftime("%d.%m.%Y"), "Branche": branche if case == "devoir" else "", "Remarque": remarque, "DateAvis": format_date_long(_date.today()), "Profs": prof_name or "", } if target_date_field: field_values[target_date_field] = probleme_date.strftime("%d.%m.%Y") for page in writer.pages: try: writer.update_page_form_field_values( page, field_values, auto_regenerate=False, ) except Exception: pass # 4. Checkboxes case_to_field = { "devoir": "CaseDevoir", "comportement": "CaseComportement", "retard": "CaseRetard", } target_check = case_to_field.get(case) for fname in case_to_field.values(): try: _set_checkbox(writer, fname, fname == target_check) except Exception: pass # 5. Force NeedAppearances pour que les viewers redessinent les valeurs try: root = writer._root_object if "/AcroForm" in root: root["/AcroForm"].update({ pypdf.generic.NameObject("/NeedAppearances"): pypdf.generic.BooleanObject(True) }) except Exception: pass # 6. Écriture (formulaire préservé éditable) buf = io.BytesIO() writer.write(buf) return buf.getvalue() def _split_date_field(writer: pypdf.PdfWriter) -> None: """Sépare le champ `Date` (avec 3 widgets enfants) en 3 champs indépendants. Renomme les widgets selon leur position Y (ordre du haut vers le bas) : kid #0 (haut) → Date_devoir kid #1 (milieu) → Date_comportement kid #2 (bas) → Date_retard """ NameObject = pypdf.generic.NameObject acroform_ref = writer._root_object.get("/AcroForm") if not acroform_ref: return acroform = acroform_ref.get_object() if hasattr(acroform_ref, "get_object") else acroform_ref fields = acroform.get("/Fields") or [] date_field = None date_ref = None for f in fields: if f.get_object().get("/T") == "Date": date_field = f.get_object() date_ref = f break if date_field is None: return kids = date_field.get("/Kids") or [] if not kids: return # Trier les enfants par Y descendant indexed = [] for kid in kids: ko = kid.get_object() rect = ko.get("/Rect") y = float(rect[1]) if rect else 0.0 indexed.append((y, kid, ko)) indexed.sort(key=lambda t: -t[0]) # Promouvoir chaque enfant en champ indépendant new_fields = [] suffixes_by_order = ["Date_devoir", "Date_comportement", "Date_retard"] for i, (_y, kid_ref, kid_obj) in enumerate(indexed): # Renomme : donne un /T propre à l'ancien widget enfant kid_obj[NameObject("/T")] = pypdf.generic.create_string_object( suffixes_by_order[i] ) # Hériter du /FT, /DA, /Q du parent si manquant sur l'enfant for prop in ("/FT", "/DA", "/Q"): if prop not in kid_obj and prop in date_field: kid_obj[NameObject(prop)] = date_field[prop] # Détacher du parent if "/Parent" in kid_obj: del kid_obj[NameObject("/Parent")] new_fields.append(kid_ref) # Retirer l'ancien champ Date de /Fields, ajouter les 3 nouveaux new_field_list = [f for f in fields if f is not date_ref] + new_fields acroform[NameObject("/Fields")] = pypdf.generic.ArrayObject(new_field_list) def _find_field(writer: pypdf.PdfWriter, name: str): acroform = writer._root_object.get("/AcroForm") if not acroform: return None for f in acroform.get("/Fields") or []: obj = f.get_object() if obj.get("/T") == name: return obj return None def _set_checkbox(writer: pypdf.PdfWriter, field_name: str, checked: bool) -> None: """Coche/décoche une checkbox AcroForm, gère les widgets enfants sans /T.""" NameObject = pypdf.generic.NameObject field = _find_field(writer, field_name) if field is None: return kids = field.get("/Kids") widgets = [k.get_object() for k in kids] if kids else [field] on_value = "/Yes" for widget in widgets: ap = widget.get("/AP") or field.get("/AP") if ap is not None: n_ap = ap.get("/N") if hasattr(ap, "get") else None if n_ap is not None: for k in n_ap.keys(): ks = str(k) if ks not in ("/Off", "Off"): on_value = ks if ks.startswith("/") else f"/{ks}" break new_val = NameObject(on_value if checked else "/Off") widget[NameObject("/AS")] = new_val field[NameObject("/V")] = NameObject(on_value if checked else "/Off")