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>
616 lines
23 KiB
Python
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}")
|