Files
Aivanov_scan_ogc/generate_pdf.py
Dom 0c0f62fbf1 feat: extraction OGC et génération de PDFs propres
Pipeline complet pour extraire les données structurées des fiches OGC
scannées (recueil praticien conseil + concertation) et générer des PDFs
propres et lisibles à partir des JSON extraits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:12:21 +01:00

616 lines
23 KiB
Python

"""
Génération de PDFs propres à partir des données OGC extraites
Reproduit fidèlement le formulaire "Fiche médicale de recueil du praticien conseil"
"""
import os
import json
import glob
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm, cm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import (
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, KeepTogether
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
# ============================================================
# Styles
# ============================================================
PAGE_W, PAGE_H = A4 # 210 x 297 mm
styles = getSampleStyleSheet()
STYLE_TITLE = ParagraphStyle(
"title_ogc", parent=styles["Normal"],
fontSize=9, fontName="Helvetica-Bold", alignment=TA_CENTER, leading=11,
)
STYLE_SUBTITLE = ParagraphStyle(
"subtitle_ogc", parent=styles["Normal"],
fontSize=6.5, fontName="Helvetica-Oblique", alignment=TA_CENTER, leading=8,
)
STYLE_HEADER = ParagraphStyle(
"header_ogc", parent=styles["Normal"],
fontSize=7.5, fontName="Helvetica-Bold", alignment=TA_LEFT, leading=9,
)
STYLE_CELL = ParagraphStyle(
"cell_ogc", parent=styles["Normal"],
fontSize=7, fontName="Helvetica", alignment=TA_CENTER, leading=9,
)
STYLE_CELL_LEFT = ParagraphStyle(
"cell_left_ogc", parent=styles["Normal"],
fontSize=7, fontName="Helvetica", alignment=TA_LEFT, leading=9,
)
STYLE_CELL_BOLD = ParagraphStyle(
"cell_bold_ogc", parent=styles["Normal"],
fontSize=7, fontName="Helvetica-Bold", alignment=TA_CENTER, leading=9,
)
STYLE_CELL_SMALL = ParagraphStyle(
"cell_small_ogc", parent=styles["Normal"],
fontSize=6, fontName="Helvetica", alignment=TA_CENTER, leading=7.5,
)
STYLE_SECTION = ParagraphStyle(
"section_ogc", parent=styles["Normal"],
fontSize=7.5, fontName="Helvetica-Bold", alignment=TA_CENTER,
leading=9, backColor=colors.Color(0.85, 0.85, 0.85),
)
STYLE_BODY = ParagraphStyle(
"body_ogc", parent=styles["Normal"],
fontSize=7, fontName="Helvetica", alignment=TA_LEFT, leading=9,
spaceBefore=2, spaceAfter=2,
)
STYLE_FOOTER = ParagraphStyle(
"footer_ogc", parent=styles["Normal"],
fontSize=5.5, fontName="Helvetica-Oblique", alignment=TA_LEFT, leading=7,
)
# Couleurs
GREY_BG = colors.Color(0.90, 0.90, 0.90)
DARK_HEADER = colors.Color(0.18, 0.31, 0.59)
WHITE = colors.white
BLACK = colors.black
def P(text, style=STYLE_CELL):
"""Raccourci pour créer un Paragraph"""
return Paragraph(str(text) if text else "", style)
def checkbox(checked=False):
"""Retourne un caractère checkbox via symbole textuel"""
return "<b>[X]</b>" if checked else "[ ]"
DECISION_LABELS = {
"maintien_avis_controleur": "Maintien de l'avis initial du médecin contrôleur",
"retour_groupage_dim": "Retour groupage initial DIM",
"autre_groupage": "Autre groupage",
}
# ============================================================
# Page Recueil
# ============================================================
def build_page_recueil(p):
"""Construit la page recueil à partir des données parsed"""
elements = []
# --- Titre ---
elements.append(P("FICHE MÉDICALE DE RECUEIL DU PRATICIEN CONSEIL (une fiche par RUM)", STYLE_TITLE))
elements.append(P("(à transmettre au médecin du DIM avant concertation)<br/>Seul le recodage impactant la facturation est renseigné", STYLE_SUBTITLE))
elements.append(Spacer(1, 3 * mm))
# --- En-tête établissement ---
header_data = [
[
P(f"<b>Établissement :</b> {p.get('etablissement', '')}", STYLE_CELL_LEFT),
P(f"<b>FINESS :</b> {p.get('finess', '')}", STYLE_CELL_LEFT),
P(f"<b>Date début contrôle :</b> {p.get('date_debut_controle', '')}", STYLE_CELL_LEFT),
],
]
t = Table(header_data, colWidths=[85 * mm, 50 * mm, 55 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.5, BLACK),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
]))
elements.append(t)
# --- N° champ + libellé + N° OGC ---
row2_data = [
[
P(f"<b>N° champ :</b> {p.get('n_champ', '')}", STYLE_CELL_LEFT),
P(f"<b>Libellé champ de contrôle :</b> Champ 1 : séjours ayant des caractéristiques communes<br/>séjours de niveau 2 3 et 4 mono RUM", STYLE_CELL_LEFT),
P(f"<b>N° OGC :</b> {p.get('n_ogc', '')}", STYLE_CELL_LEFT),
],
]
t = Table(row2_data, colWidths=[30 * mm, 130 * mm, 30 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.5, BLACK),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
]))
elements.append(t)
# --- Dates du séjour ---
elements.append(Spacer(1, 1 * mm))
dates_data = [[
P(f"<b>Dossier manquant :</b> 0", STYLE_CELL_LEFT),
P(f"<b>Dates du séjour :</b> {p.get('dates_sejour', '')}", STYLE_CELL_LEFT),
]]
t = Table(dates_data, colWidths=[50 * mm, 140 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.5, BLACK),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
]))
elements.append(t)
elements.append(Spacer(1, 2 * mm))
# --- Données du séjour ---
se = p.get("sejour_etab", {})
sr = p.get("sejour_reco", {})
col_hdrs = ["", "Âge", "Sexe", "Date\nnais.", "Durée\nséjour", "Mode\nentrée",
"Prove-\nnance", "Mode\nsortie", "Desti-\nnation", "Nb RUM", "Nb DPI", "Nb\nséances",
"Psy\nE&B", "Psy\nD.S"]
col_widths = [22 * mm] + [12 * mm] * 13
sejour_data = [
[P("<b>Données du\nséjour</b>", STYLE_CELL_SMALL)] + [P(h, STYLE_CELL_SMALL) for h in col_hdrs[1:]],
[P("<b>Établissement</b>", STYLE_CELL_SMALL),
P(se.get("age", ""), STYLE_CELL), P(se.get("sexe", ""), STYLE_CELL),
P("", STYLE_CELL), P(se.get("duree_sejour", ""), STYLE_CELL),
P(se.get("mode_entree", ""), STYLE_CELL), P(se.get("provenance", ""), STYLE_CELL),
P(se.get("mode_sortie", ""), STYLE_CELL), P(se.get("destination", ""), STYLE_CELL),
P("", STYLE_CELL), P("", STYLE_CELL), P("", STYLE_CELL),
P("", STYLE_CELL), P("", STYLE_CELL)],
[P("<b>Recodage</b>", STYLE_CELL_SMALL),
P(sr.get("age", ""), STYLE_CELL), P(sr.get("sexe", ""), STYLE_CELL),
P("", STYLE_CELL), P(sr.get("duree_sejour", ""), STYLE_CELL),
P(sr.get("mode_entree", ""), STYLE_CELL), P(sr.get("provenance", ""), STYLE_CELL),
P(sr.get("mode_sortie", ""), STYLE_CELL), P(sr.get("destination", ""), STYLE_CELL),
P("", STYLE_CELL), P("", STYLE_CELL), P("", STYLE_CELL),
P("", STYLE_CELL), P("", STYLE_CELL)],
]
t = Table(sejour_data, colWidths=col_widths)
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.3, BLACK),
("BACKGROUND", (0, 0), (-1, 0), GREY_BG),
("BACKGROUND", (0, 1), (0, 2), GREY_BG),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 1),
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
]))
elements.append(t)
elements.append(Spacer(1, 2 * mm))
# --- Données du RUM ---
rum = p.get("rum_etab", {})
rum_header = [
[P("<b>Données du RUM</b>", STYLE_CELL_SMALL),
P("Éts détails SP", STYLE_CELL_SMALL),
P("<b>UM</b>", STYLE_CELL_SMALL),
P("<b>IGS II</b>", STYLE_CELL_SMALL),
P("<b>Durée RUM</b>", STYLE_CELL_SMALL),
P("Nature\nsuppl.", STYLE_CELL_SMALL),
P("Nb\nsuppl.", STYLE_CELL_SMALL)],
]
t = Table(rum_header, colWidths=[30 * mm, 30 * mm, 25 * mm, 25 * mm, 30 * mm, 25 * mm, 25 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.3, BLACK),
("BACKGROUND", (0, 0), (-1, 0), GREY_BG),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 1),
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
]))
elements.append(t)
# Lignes RUM Établissement et Recodage
rum_lines = [
[P(f"<b>N° RUM Établissement : 1/1</b>", STYLE_CELL_LEFT),
P("0", STYLE_CELL),
P(rum.get("um", ""), STYLE_CELL),
P(rum.get("igs", ""), STYLE_CELL),
P(rum.get("duree", ""), STYLE_CELL),
P(f"{rum.get('dates', '')}", STYLE_CELL_LEFT)],
[P(f"<b>N° RUM Recodage : 1/1</b>", STYLE_CELL_LEFT),
P("0", STYLE_CELL),
P(rum.get("um", ""), STYLE_CELL),
P("", STYLE_CELL),
P(rum.get("duree", ""), STYLE_CELL),
P(f"{rum.get('dates', '')}", STYLE_CELL_LEFT)],
]
t = Table(rum_lines, colWidths=[45 * mm, 15 * mm, 25 * mm, 25 * mm, 25 * mm, 55 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.3, BLACK),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 1),
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
]))
elements.append(t)
elements.append(Spacer(1, 2 * mm))
# --- Codage ---
ce = p.get("codage_etab", {})
cr = p.get("codage_reco", {})
# Section header
codage_header = [[
P("<b>Codage de l'Établissement</b>", STYLE_SECTION),
P("<b>Recodage</b>", STYLE_SECTION),
]]
t = Table(codage_header, colWidths=[150 * mm, 40 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.5, BLACK),
("BACKGROUND", (0, 0), (-1, 0), GREY_BG),
]))
elements.append(t)
# DP
dp_data = [
[P("<b>DP</b>", STYLE_CELL_BOLD), P(ce.get("dp", ""), STYLE_CELL),
P(ce.get("dp_libelle", ""), STYLE_CELL_LEFT),
P(cr.get("dp", ""), STYLE_CELL)],
]
t = Table(dp_data, colWidths=[15 * mm, 25 * mm, 110 * mm, 40 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.3, BLACK),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 1),
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
]))
elements.append(t)
# DR
dr_data = [
[P("<b>DR</b>", STYLE_CELL_BOLD), P(ce.get("dr", ""), STYLE_CELL),
P("", STYLE_CELL_LEFT),
P(cr.get("dr", ""), STYLE_CELL)],
]
t = Table(dr_data, colWidths=[15 * mm, 25 * mm, 110 * mm, 40 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.3, BLACK),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 1),
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
]))
elements.append(t)
# DAS
das_etab = ce.get("das", [])
das_reco = cr.get("das", [])
max_das = max(len(das_etab), len(das_reco), 1)
das_rows = [[P("<b>DAS</b>", STYLE_CELL_BOLD),
P("<b>Code</b>", STYLE_CELL_SMALL), P("<b>Pos</b>", STYLE_CELL_SMALL),
P("<b>Libellé</b>", STYLE_CELL_SMALL),
P("<b>Code</b>", STYLE_CELL_SMALL), P("<b>Pos</b>", STYLE_CELL_SMALL)]]
for i in range(max_das):
de = das_etab[i] if i < len(das_etab) else {}
dr_item = das_reco[i] if i < len(das_reco) else {}
if isinstance(de, str):
de = {"code": de}
if isinstance(dr_item, str):
dr_item = {"code": dr_item}
das_rows.append([
P("", STYLE_CELL),
P(de.get("code", ""), STYLE_CELL),
P(de.get("position", ""), STYLE_CELL),
P(de.get("libelle", ""), STYLE_CELL_LEFT),
P(dr_item.get("code", ""), STYLE_CELL),
P(dr_item.get("position", ""), STYLE_CELL),
])
# Lignes vides si moins de 6 DAS
for _ in range(max(0, 6 - max_das)):
das_rows.append([P("", STYLE_CELL)] * 6)
t = Table(das_rows, colWidths=[15 * mm, 25 * mm, 12 * mm, 98 * mm, 25 * mm, 15 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.3, BLACK),
("BACKGROUND", (0, 0), (-1, 0), GREY_BG),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 1),
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
("SPAN", (0, 0), (0, 0)),
]))
elements.append(t)
elements.append(Spacer(1, 2 * mm))
# --- Actes ---
actes_etab = p.get("actes_etab", [])
actes_reco = p.get("actes_reco", [])
max_actes = max(len(actes_etab), len(actes_reco), 1)
actes_rows = [[P("<b>Actes</b>", STYLE_CELL_BOLD),
P("<b>Code</b>", STYLE_CELL_SMALL), P("<b>Pos</b>", STYLE_CELL_SMALL),
P("<b>Libellé</b>", STYLE_CELL_SMALL),
P("<b>Code</b>", STYLE_CELL_SMALL), P("<b>Pos</b>", STYLE_CELL_SMALL)]]
for i in range(max_actes):
ae = actes_etab[i] if i < len(actes_etab) else {}
ar = actes_reco[i] if i < len(actes_reco) else {}
if isinstance(ae, str):
ae = {"code": ae}
if isinstance(ar, str):
ar = {"code": ar}
actes_rows.append([
P("", STYLE_CELL),
P(ae.get("code", ""), STYLE_CELL),
P(ae.get("position", ae.get("quantite", "")), STYLE_CELL),
P(ae.get("libelle", ""), STYLE_CELL_LEFT),
P(ar.get("code", ""), STYLE_CELL),
P(ar.get("position", ar.get("quantite", "")), STYLE_CELL),
])
# Lignes vides
for _ in range(max(0, 4 - max_actes)):
actes_rows.append([P("", STYLE_CELL)] * 6)
t = Table(actes_rows, colWidths=[15 * mm, 25 * mm, 12 * mm, 98 * mm, 25 * mm, 15 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.3, BLACK),
("BACKGROUND", (0, 0), (-1, 0), GREY_BG),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 1),
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
]))
elements.append(t)
elements.append(Spacer(1, 2 * mm))
# --- GHM / GHS ---
ghm_data = [[
P(f"<b>GHM établissement :</b> {p.get('ghm_etab', '')}", STYLE_CELL_LEFT),
P(f"<b>GHS établissement :</b> {p.get('ghs_etab', '')}", STYLE_CELL_LEFT),
P(f"<b>GHM après recodage :</b> {p.get('ghm_reco', '')}", STYLE_CELL_LEFT),
P(f"<b>GHS après recodage :</b> {p.get('ghs_reco', '')}", STYLE_CELL_LEFT),
]]
t = Table(ghm_data, colWidths=[50 * mm, 45 * mm, 50 * mm, 45 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.3, BLACK),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
]))
elements.append(t)
# --- Praticien conseil ---
elements.append(Spacer(1, 1 * mm))
prat_data = [[
P("<b>Praticien conseil</b>", STYLE_CELL_LEFT),
P("", STYLE_CELL),
P("<b>Médecin DIM</b>", STYLE_CELL_LEFT),
]]
t = Table(prat_data, colWidths=[50 * mm, 90 * mm, 50 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.3, BLACK),
]))
elements.append(t)
# --- Décisions ---
elements.append(Spacer(1, 1 * mm))
is_accord = p.get("accord_desaccord", "") == "accord"
is_desaccord = p.get("accord_desaccord", "") == "désaccord"
reco_imp = p.get("recodage_impactant", "")
ghs_inj = p.get("ghs_injustifie", "")
decisions_data = [
[P(f"<b>Recodage impactant la facturation :</b> {reco_imp}", STYLE_CELL_LEFT),
P(f"<b>Accord</b> {checkbox(is_accord)} <b>Désaccord</b> {checkbox(is_desaccord)}", STYLE_CELL_LEFT)],
[P(f"<b>GHS injustifié :</b> {ghs_inj}", STYLE_CELL_LEFT),
P("", STYLE_CELL)],
]
t = Table(decisions_data, colWidths=[110 * mm, 80 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.3, BLACK),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
]))
elements.append(t)
# --- Nom praticien ---
elements.append(Spacer(1, 1 * mm))
prat_nom_data = [[
P(f"<b>Nom du praticien conseil responsable du codage :</b>", STYLE_CELL_LEFT),
]]
t = Table(prat_nom_data, colWidths=[190 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
]))
elements.append(t)
prat_val_data = [[P(f"<b>{p.get('praticien_conseil', '')}</b>", STYLE_CELL_LEFT)]]
t = Table(prat_val_data, colWidths=[190 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
]))
elements.append(t)
# --- Footer ---
elements.append(Spacer(1, 2 * mm))
elements.append(P(
"En fonction des DR/DR et actes retenus par le PC, seul le recodage d'une des CMA les plus élevées ayant une incidence "
"sur le GHM est sur la facturation des suppléments<br/>"
"sera renseigné. Hors RCI injustifié avec actes externes, seuls les actes classants seront recueillis.",
STYLE_FOOTER
))
return elements
# ============================================================
# Page Concertation 2/2
# ============================================================
def build_page_concertation_2(p2):
"""Page décision finale concertation"""
elements = []
elements.append(P("<b>CONCERTATION 2/2 — Décision finale</b>", STYLE_TITLE))
elements.append(Spacer(1, 5 * mm))
# GHS
ghs_data = [[
P(f"<b>GHS initial :</b> {p2.get('ghs_initial', '')}", STYLE_CELL_LEFT),
P(f"<b>GHS avant concertation :</b> {p2.get('ghs_avant_concertation', '')}", STYLE_CELL_LEFT),
P(f"<b>GHS final après concertation :</b> {p2.get('ghs_final', '')}", STYLE_CELL_LEFT),
]]
t = Table(ghs_data, colWidths=[63 * mm, 63 * mm, 64 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("INNERGRID", (0, 0), (-1, -1), 0.5, BLACK),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
]))
elements.append(t)
elements.append(Spacer(1, 3 * mm))
# Décision
decision_raw = p2.get("decision", "")
decision = DECISION_LABELS.get(decision_raw, decision_raw)
dec_data = [[
P(f"<b>Décision :</b> {decision}", STYLE_CELL_LEFT),
]]
t = Table(dec_data, colWidths=[190 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
]))
elements.append(t)
elements.append(Spacer(1, 3 * mm))
# Date
date_conc = p2.get("date_concertation", "")
if date_conc:
elements.append(P(f"<b>Date de concertation :</b> {date_conc}", STYLE_CELL_LEFT))
return elements
# ============================================================
# Page Concertation 1/2 (Argumentaire)
# ============================================================
def build_page_concertation_1(p1):
"""Page argumentaire"""
elements = []
elements.append(P("<b>CONCERTATION 1/2 — Argumentaire du médecin contrôleur</b>", STYLE_TITLE))
elements.append(Spacer(1, 5 * mm))
date_conc = p1.get("date_concertation", "")
if date_conc:
elements.append(P(f"<b>Date :</b> {date_conc}", STYLE_CELL_LEFT))
elements.append(Spacer(1, 3 * mm))
arg = p1.get("argumentaire", "")
if arg:
# Nettoyer les séparateurs | et formater
arg_clean = arg.replace(" | ", "\n").replace("| ", "\n")
elements.append(P(f"<b>Argumentaire :</b>", STYLE_CELL_LEFT))
elements.append(Spacer(1, 2 * mm))
arg_data = [[P(arg_clean, STYLE_BODY)]]
t = Table(arg_data, colWidths=[190 * mm])
t.setStyle(TableStyle([
("BOX", (0, 0), (-1, -1), 0.5, BLACK),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 5),
("RIGHTPADDING", (0, 0), (-1, -1), 5),
]))
elements.append(t)
return elements
# ============================================================
# Génération du PDF complet
# ============================================================
def generate_ogc_pdf(result, output_path):
"""Génère un PDF propre à partir des données extraites"""
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
leftMargin=10 * mm, rightMargin=10 * mm,
topMargin=10 * mm, bottomMargin=10 * mm,
)
elements = []
# Page recueil
rec = result.get("recueil")
if rec:
p = rec.get("parsed", {})
elements.extend(build_page_recueil(p))
# Page concertation (argumentaire + décision sur une seule page)
c1 = result.get("concertation_1")
c2 = result.get("concertation_2")
if c1 or c2:
from reportlab.platypus import PageBreak
elements.append(PageBreak())
if c1:
p1 = c1.get("parsed", {})
elements.extend(build_page_concertation_1(p1))
if c2:
elements.append(Spacer(1, 5 * mm))
p2 = c2.get("parsed", {})
elements.extend(build_page_concertation_2(p2))
doc.build(elements)
# ============================================================
# Main
# ============================================================
if __name__ == "__main__":
output_dir = "/home/dom/ai/Aivanov_scan_ogc/output"
pdf_output_dir = os.path.join(output_dir, "pdf_propres")
os.makedirs(pdf_output_dir, exist_ok=True)
json_files = sorted(glob.glob(os.path.join(output_dir, "OGC *.json")))
print(f"Génération de {len(json_files)} PDFs propres...")
for json_path in json_files:
basename = os.path.splitext(os.path.basename(json_path))[0]
with open(json_path, "r", encoding="utf-8") as f:
result = json.load(f)
pdf_path = os.path.join(pdf_output_dir, f"{basename} - propre.pdf")
try:
generate_ogc_pdf(result, pdf_path)
print(f"{basename}")
except Exception as e:
print(f"{basename}: {e}")
import traceback
traceback.print_exc()
print(f"\nPDFs générés dans: {pdf_output_dir}")