Compare commits
47 Commits
main
...
a827d860f1
| Author | SHA1 | Date | |
|---|---|---|---|
| a827d860f1 | |||
| eb14cd219d | |||
| c9572c383a | |||
| 274e2fa586 | |||
| 7a2af5c905 | |||
| 4488a1d4a0 | |||
| 19e089ea38 | |||
| 26b210607c | |||
| 6e0e8c7312 | |||
| 26ac02b0cb | |||
| 782551c1c6 | |||
| 8629a0cda0 | |||
| e967a67052 | |||
| bc2fe667a0 | |||
| f9532d5543 | |||
| 4e6fd97e84 | |||
| cede2d64d6 | |||
| 98a21d7ccc | |||
| ea761823d6 | |||
| 47a71df930 | |||
| 93617bab55 | |||
| dfa6e2957b | |||
| eb797a4761 | |||
| 85e19af655 | |||
| d6915247fe | |||
| bf30f622d9 | |||
| b46ea83900 | |||
| 5163cb1657 | |||
| 09231be5e8 | |||
| 3b1f6cdfbe | |||
| 78adb3ba70 | |||
| 63bd4ace1d | |||
| ee34042179 | |||
| 883f14ab79 | |||
| f92da4d54e | |||
| 871221ea56 | |||
| f188116bc1 | |||
| 6806aee587 | |||
| 70ff0b9e12 | |||
| dfa45041d7 | |||
| 4eba826ca5 | |||
| 0ba5424eb0 | |||
| 99b6e7f1d1 | |||
| 30a6ebcc19 | |||
| f61e767ee6 | |||
| c78f9f415d | |||
| 340348b820 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -40,13 +40,6 @@ models/
|
|||||||
*.jpg
|
*.jpg
|
||||||
*.jpeg
|
*.jpeg
|
||||||
*.gif
|
*.gif
|
||||||
# Exception : assets embarqués dans l'exe (splash, icônes…) doivent être versionnés
|
|
||||||
!assets/**
|
|
||||||
!assets
|
|
||||||
|
|
||||||
# build_info.py : régénéré automatiquement par scripts/rebuild_anon.ps1
|
|
||||||
# avec date/commit/branch. Ne pas versionner.
|
|
||||||
build_info.py
|
|
||||||
*.mp3
|
*.mp3
|
||||||
*.wav
|
*.wav
|
||||||
*.mp4
|
*.mp4
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import queue
|
|||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
import threading
|
import threading
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -83,80 +82,17 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
sv_ttk = None
|
sv_ttk = None
|
||||||
|
|
||||||
# PIL pour charger le logo / icônes (optionnel — dégradation si absent).
|
|
||||||
try:
|
|
||||||
from PIL import Image, ImageTk
|
|
||||||
_PIL_AVAILABLE = True
|
|
||||||
except Exception:
|
|
||||||
_PIL_AVAILABLE = False
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Constantes
|
# Constantes
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
APP_TITLE = "Pseudonymisation de vos documents"
|
APP_TITLE = "Pseudonymisation de PDF"
|
||||||
APP_VERSION = "v5.5"
|
APP_VERSION = "v5.0"
|
||||||
|
|
||||||
# Métadonnées de build — chargées depuis build_info.py (régénéré par rebuild_anon.ps1)
|
|
||||||
try:
|
|
||||||
from build_info import BUILD_DATE, BUILD_COMMIT, BUILD_BRANCH
|
|
||||||
except Exception:
|
|
||||||
BUILD_DATE = "dev"
|
|
||||||
BUILD_COMMIT = "dev"
|
|
||||||
BUILD_BRANCH = "dev"
|
|
||||||
|
|
||||||
|
|
||||||
def _version_long() -> str:
|
|
||||||
"""Version étendue : v5.4 · 2026-04-15 18:15 · 234137e"""
|
|
||||||
parts = [APP_VERSION]
|
|
||||||
if BUILD_DATE != "dev":
|
|
||||||
parts.append(BUILD_DATE)
|
|
||||||
if BUILD_COMMIT != "dev":
|
|
||||||
parts.append(f"#{BUILD_COMMIT}")
|
|
||||||
return " · ".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def _asset(name: str) -> Path:
|
|
||||||
"""Résout le chemin d'un asset dans assets/ (compatible frozen PyInstaller)."""
|
|
||||||
if getattr(sys, 'frozen', False):
|
|
||||||
base = Path(sys._MEIPASS)
|
|
||||||
else:
|
|
||||||
base = Path(__file__).resolve().parent
|
|
||||||
return base / 'assets' / name
|
|
||||||
|
|
||||||
def _app_dir() -> Path:
|
def _app_dir() -> Path:
|
||||||
"""Répertoire racine de l'application (compatible PyInstaller/Nuitka)."""
|
"""Répertoire racine de l'application (compatible Nuitka standalone)."""
|
||||||
if getattr(sys, 'frozen', False):
|
|
||||||
return Path(sys._MEIPASS)
|
|
||||||
return Path(__file__).resolve().parent
|
return Path(__file__).resolve().parent
|
||||||
|
|
||||||
def _exe_dir() -> Path:
|
DEFAULT_CFG = _app_dir() / "config" / "dictionnaires.yml"
|
||||||
"""Répertoire de l'exécutable (pour les fichiers persistants : config, logs)."""
|
|
||||||
if getattr(sys, 'frozen', False):
|
|
||||||
return Path(sys.executable).parent
|
|
||||||
return Path(__file__).resolve().parent
|
|
||||||
|
|
||||||
def _resolve_config() -> Path:
|
|
||||||
"""Cherche la config en priorité à côté de l'exe, sinon dans l'app embarquée.
|
|
||||||
|
|
||||||
Si le fichier n'existe pas à côté de l'exe, copie la version embarquée
|
|
||||||
pour que l'utilisateur puisse la modifier sans recompiler.
|
|
||||||
"""
|
|
||||||
exe_cfg = _exe_dir() / "config" / "dictionnaires.yml"
|
|
||||||
app_cfg = _app_dir() / "config" / "dictionnaires.yml"
|
|
||||||
|
|
||||||
if exe_cfg.exists():
|
|
||||||
return exe_cfg
|
|
||||||
|
|
||||||
# Premier lancement : copier la config embarquée à côté de l'exe
|
|
||||||
if app_cfg.exists():
|
|
||||||
exe_cfg.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
import shutil
|
|
||||||
shutil.copy2(str(app_cfg), str(exe_cfg))
|
|
||||||
return exe_cfg
|
|
||||||
|
|
||||||
return app_cfg # fallback
|
|
||||||
|
|
||||||
DEFAULT_CFG = _resolve_config()
|
|
||||||
MODELS_DIR = _app_dir() / "models"
|
MODELS_DIR = _app_dir() / "models"
|
||||||
|
|
||||||
DEFAULTS_CFG_TEXT = r"""
|
DEFAULTS_CFG_TEXT = r"""
|
||||||
@@ -184,27 +120,19 @@ flags:
|
|||||||
regex_engine: "python"
|
regex_engine: "python"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Palette dérivée du logo aivanonym (gradient magenta → rose → pêche → noir)
|
# Couleurs
|
||||||
# Magenta du logo : primaire (boutons, accents)
|
CLR_PRIMARY = "#2563eb"
|
||||||
# Pêche : secondaire (tags, highlights)
|
CLR_PRIMARY_LIGHT = "#dbeafe"
|
||||||
# Noir/gris : texte et neutres
|
CLR_GREEN = "#16a34a"
|
||||||
# Blanc/gris clair : fonds
|
CLR_GREEN_LIGHT = "#dcfce7"
|
||||||
CLR_PRIMARY = "#E91E63" # magenta logo (CTA, liens)
|
CLR_RED = "#dc2626"
|
||||||
CLR_PRIMARY_DARK = "#C2185B" # hover / pressed
|
CLR_RED_LIGHT = "#fee2e2"
|
||||||
CLR_PRIMARY_LIGHT = "#FCE4EC" # fond léger (cartes sélectionnées)
|
CLR_BLUE_LIGHT = "#eff6ff"
|
||||||
CLR_ACCENT = "#FFB74D" # pêche logo (tags secondaires)
|
CLR_CARD_BG = "#ffffff"
|
||||||
CLR_ACCENT_LIGHT = "#FFF3E0" # fond accent léger
|
CLR_CARD_BORDER = "#d1d5db"
|
||||||
CLR_GREEN = "#2E7D32" # succès
|
CLR_BG = "#f9fafb"
|
||||||
CLR_GREEN_LIGHT = "#E8F5E9"
|
CLR_TEXT = "#111827"
|
||||||
CLR_RED = "#C62828" # erreur / danger
|
CLR_TEXT_SECONDARY = "#6b7280"
|
||||||
CLR_RED_LIGHT = "#FFEBEE"
|
|
||||||
CLR_BLUE_LIGHT = "#FCE4EC" # conservé pour compat (remappé vers primary_light)
|
|
||||||
CLR_CARD_BG = "#FFFFFF"
|
|
||||||
CLR_CARD_BORDER = "#E0E0E0"
|
|
||||||
CLR_BG = "#FAFAFA" # fond principal (gris très clair)
|
|
||||||
CLR_TEXT = "#212121" # quasi-noir (du logo)
|
|
||||||
CLR_TEXT_SECONDARY = "#757575" # gris moyen
|
|
||||||
CLR_DIVIDER = "#EEEEEE"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Messages worker → UI
|
# Messages worker → UI
|
||||||
@@ -325,22 +253,10 @@ class ToolTip:
|
|||||||
class App:
|
class App:
|
||||||
def __init__(self, root: tk.Tk):
|
def __init__(self, root: tk.Tk):
|
||||||
self.root = root
|
self.root = root
|
||||||
# Titre avec version longue pour identifier la build au premier coup d'œil
|
self.root.title(APP_TITLE)
|
||||||
# (évite les confusions entre exe ancien/nouveau lors des tests).
|
|
||||||
self.root.title(f"{APP_TITLE} — {_version_long()}")
|
|
||||||
self.root.geometry("780x820")
|
self.root.geometry("780x820")
|
||||||
self.root.minsize(600, 650)
|
self.root.minsize(600, 650)
|
||||||
|
|
||||||
# Icône de la fenêtre (coin haut-gauche + taskbar Windows).
|
|
||||||
# En mode dev (Linux) tkinter lit iconphoto PNG ; sur Windows, iconbitmap
|
|
||||||
# accepte .ico. On tente les deux pour couvrir.
|
|
||||||
self._icon_refs: list = [] # refs pour éviter garbage collection
|
|
||||||
self._apply_window_icon()
|
|
||||||
|
|
||||||
# Préchargement logo pour l'en-tête (besoin de ref persistante sinon
|
|
||||||
# tkinter nettoie l'image → label blanc).
|
|
||||||
self._logo_img = self._load_image_safe(_asset('logo_header.png'))
|
|
||||||
|
|
||||||
# --- Thème ---
|
# --- Thème ---
|
||||||
self._apply_theme()
|
self._apply_theme()
|
||||||
|
|
||||||
@@ -389,13 +305,9 @@ class App:
|
|||||||
|
|
||||||
# --- Contrôle d'arrêt ---
|
# --- Contrôle d'arrêt ---
|
||||||
self._stop_requested = False
|
self._stop_requested = False
|
||||||
# --- Fichier unique (None = mode dossier) ---
|
|
||||||
self._single_file: Optional[Path] = None
|
|
||||||
|
|
||||||
# --- Construction UI ---
|
# --- Construction UI ---
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
# Afficher l'onglet Anonymisation par défaut
|
|
||||||
self._switch_tab("anonym")
|
|
||||||
self._pump_logs()
|
self._pump_logs()
|
||||||
self._ensure_cfg_exists()
|
self._ensure_cfg_exists()
|
||||||
self._load_cfg()
|
self._load_cfg()
|
||||||
@@ -403,63 +315,6 @@ class App:
|
|||||||
# --- Chargement automatique du modèle NER ---
|
# --- Chargement automatique du modèle NER ---
|
||||||
self._auto_load_ner()
|
self._auto_load_ner()
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
# Onglets custom
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
def _switch_tab(self, name: str):
|
|
||||||
"""Affiche l'onglet nommé, met à jour les styles des boutons."""
|
|
||||||
if name not in self._tab_frames:
|
|
||||||
return
|
|
||||||
# Cacher tous les contenus
|
|
||||||
for frame in self._tab_frames.values():
|
|
||||||
frame.pack_forget()
|
|
||||||
# Afficher l'onglet demandé
|
|
||||||
self._tab_frames[name].pack(fill=tk.BOTH, expand=True)
|
|
||||||
# Mettre à jour les styles des boutons d'onglets
|
|
||||||
for tab_name, widgets in self._tab_buttons.items():
|
|
||||||
if tab_name == name:
|
|
||||||
widgets["label"].configure(fg=CLR_PRIMARY, bg=CLR_BG)
|
|
||||||
widgets["underline"].configure(bg=CLR_PRIMARY)
|
|
||||||
else:
|
|
||||||
widgets["label"].configure(fg=CLR_TEXT_SECONDARY, bg=CLR_BG)
|
|
||||||
widgets["underline"].configure(bg=CLR_BG)
|
|
||||||
self._active_tab = name
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
# Icônes & assets
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
def _apply_window_icon(self):
|
|
||||||
"""Définit l'icône de la fenêtre. Windows : .ico préférable ; Linux : PNG."""
|
|
||||||
try:
|
|
||||||
ico = _asset('icons/app.ico')
|
|
||||||
if sys.platform == 'win32' and ico.exists():
|
|
||||||
try:
|
|
||||||
self.root.iconbitmap(str(ico))
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# Fallback : iconphoto PNG (toutes plateformes)
|
|
||||||
png = _asset('icons/icon_128.png')
|
|
||||||
if png.exists() and _PIL_AVAILABLE:
|
|
||||||
img = Image.open(png)
|
|
||||||
photo = ImageTk.PhotoImage(img)
|
|
||||||
self._icon_refs.append(photo)
|
|
||||||
self.root.iconphoto(True, photo)
|
|
||||||
except Exception:
|
|
||||||
pass # dégradation silencieuse — l'icône n'est pas bloquante
|
|
||||||
|
|
||||||
def _load_image_safe(self, path: Path):
|
|
||||||
"""Charge une image et garde la ref pour éviter le GC. None si PIL absent."""
|
|
||||||
if not _PIL_AVAILABLE or not path.exists():
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
img = Image.open(path).convert('RGBA')
|
|
||||||
photo = ImageTk.PhotoImage(img)
|
|
||||||
self._icon_refs.append(photo)
|
|
||||||
return photo
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
# Thème
|
# Thème
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
@@ -479,89 +334,15 @@ class App:
|
|||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
self.root.configure(bg=CLR_BG)
|
self.root.configure(bg=CLR_BG)
|
||||||
pad_x = 32
|
|
||||||
|
|
||||||
# =============================================================
|
# Conteneur scrollable
|
||||||
# HEADER fixe (logo + titre + baseline), hors onglets
|
outer = tk.Frame(self.root, bg=CLR_BG)
|
||||||
# =============================================================
|
outer.pack(fill=tk.BOTH, expand=True)
|
||||||
header = tk.Frame(self.root, bg=CLR_BG)
|
|
||||||
header.pack(fill=tk.X, padx=pad_x, pady=(16, 8))
|
|
||||||
|
|
||||||
if self._logo_img is not None:
|
canvas = tk.Canvas(outer, bg=CLR_BG, highlightthickness=0)
|
||||||
tk.Label(header, image=self._logo_img, bg=CLR_BG).pack(anchor="w")
|
scrollbar = ttk.Scrollbar(outer, orient=tk.VERTICAL, command=canvas.yview)
|
||||||
else:
|
|
||||||
tk.Label(header, text="aivanonym", font=(self._font_family, 22, "bold"),
|
|
||||||
bg=CLR_BG, fg=CLR_PRIMARY).pack(anchor="w")
|
|
||||||
|
|
||||||
tk.Label(
|
|
||||||
header,
|
|
||||||
text="Pseudonymisation de documents médicaux — 100% local",
|
|
||||||
font=(self._font_family, 10),
|
|
||||||
bg=CLR_BG, fg=CLR_TEXT_SECONDARY, anchor="w",
|
|
||||||
).pack(fill=tk.X, pady=(4, 0))
|
|
||||||
|
|
||||||
# Ligne colorée inspirée du gradient du logo
|
|
||||||
accent_bar = tk.Frame(self.root, bg=CLR_PRIMARY, height=3)
|
|
||||||
accent_bar.pack(fill=tk.X)
|
|
||||||
|
|
||||||
# =============================================================
|
|
||||||
# ONGLETS CUSTOM (boutons uniformes — rendu pro)
|
|
||||||
# Remplace ttk.Notebook dont les onglets ont des tailles/styles
|
|
||||||
# variables selon l'état actif. Ici : tous les onglets identiques,
|
|
||||||
# seule une bordure basse magenta signale l'onglet actif.
|
|
||||||
# =============================================================
|
|
||||||
tabs_bar = tk.Frame(self.root, bg=CLR_BG)
|
|
||||||
tabs_bar.pack(fill=tk.X, padx=0, pady=(4, 0))
|
|
||||||
|
|
||||||
self._tab_frames: dict = {} # nom → frame outer
|
|
||||||
self._tab_buttons: dict = {} # nom → dict(container, label, underline)
|
|
||||||
self._active_tab: Optional[str] = None
|
|
||||||
|
|
||||||
def _make_tab_button(parent, name: str, label: str):
|
|
||||||
"""Crée un onglet cliquable uniforme (fond, texte, underline)."""
|
|
||||||
container = tk.Frame(parent, bg=CLR_BG, cursor="hand2")
|
|
||||||
container.pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
txt = tk.Label(
|
|
||||||
container, text=label,
|
|
||||||
font=(self._font_family, 11, "bold"),
|
|
||||||
bg=CLR_BG, fg=CLR_TEXT_SECONDARY,
|
|
||||||
padx=26, pady=10, cursor="hand2",
|
|
||||||
)
|
|
||||||
txt.pack(fill=tk.X)
|
|
||||||
|
|
||||||
# Bordure basse qui devient magenta quand actif
|
|
||||||
underline = tk.Frame(container, bg=CLR_BG, height=3)
|
|
||||||
underline.pack(fill=tk.X)
|
|
||||||
|
|
||||||
def _on_click(_e=None):
|
|
||||||
self._switch_tab(name)
|
|
||||||
for w in (container, txt, underline):
|
|
||||||
w.bind("<Button-1>", _on_click)
|
|
||||||
|
|
||||||
self._tab_buttons[name] = {
|
|
||||||
"container": container, "label": txt, "underline": underline,
|
|
||||||
}
|
|
||||||
|
|
||||||
_make_tab_button(tabs_bar, "anonym", "Anonymisation")
|
|
||||||
_make_tab_button(tabs_bar, "params", "Paramètres")
|
|
||||||
|
|
||||||
# Séparateur gris clair sous les onglets
|
|
||||||
tk.Frame(self.root, bg=CLR_DIVIDER, height=1).pack(fill=tk.X)
|
|
||||||
|
|
||||||
# Conteneur des contenus (un seul visible à la fois)
|
|
||||||
tabs_content = tk.Frame(self.root, bg=CLR_BG)
|
|
||||||
tabs_content.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
tab_anonym_outer = tk.Frame(tabs_content, bg=CLR_BG)
|
|
||||||
tab_params_outer = tk.Frame(tabs_content, bg=CLR_BG)
|
|
||||||
self._tab_frames["anonym"] = tab_anonym_outer
|
|
||||||
self._tab_frames["params"] = tab_params_outer
|
|
||||||
|
|
||||||
# --- Scroll pour l'onglet Anonymisation ---
|
|
||||||
canvas = tk.Canvas(tab_anonym_outer, bg=CLR_BG, highlightthickness=0)
|
|
||||||
scrollbar = ttk.Scrollbar(tab_anonym_outer, orient=tk.VERTICAL, command=canvas.yview)
|
|
||||||
self._scroll_frame = tk.Frame(canvas, bg=CLR_BG)
|
self._scroll_frame = tk.Frame(canvas, bg=CLR_BG)
|
||||||
|
|
||||||
self._scroll_frame.bind(
|
self._scroll_frame.bind(
|
||||||
"<Configure>",
|
"<Configure>",
|
||||||
lambda e: canvas.configure(scrollregion=canvas.bbox("all")),
|
lambda e: canvas.configure(scrollregion=canvas.bbox("all")),
|
||||||
@@ -569,10 +350,12 @@ class App:
|
|||||||
canvas_window = canvas.create_window((0, 0), window=self._scroll_frame, anchor="nw")
|
canvas_window = canvas.create_window((0, 0), window=self._scroll_frame, anchor="nw")
|
||||||
canvas.configure(yscrollcommand=scrollbar.set)
|
canvas.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
# Ajuster la largeur du frame interne à celle du canvas
|
||||||
def _on_canvas_configure(event):
|
def _on_canvas_configure(event):
|
||||||
canvas.itemconfig(canvas_window, width=event.width)
|
canvas.itemconfig(canvas_window, width=event.width)
|
||||||
canvas.bind("<Configure>", _on_canvas_configure)
|
canvas.bind("<Configure>", _on_canvas_configure)
|
||||||
|
|
||||||
|
# Scroll molette
|
||||||
def _on_mousewheel(event):
|
def _on_mousewheel(event):
|
||||||
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
||||||
def _on_mousewheel_linux(event):
|
def _on_mousewheel_linux(event):
|
||||||
@@ -580,38 +363,36 @@ class App:
|
|||||||
canvas.yview_scroll(-3, "units")
|
canvas.yview_scroll(-3, "units")
|
||||||
elif event.num == 5:
|
elif event.num == 5:
|
||||||
canvas.yview_scroll(3, "units")
|
canvas.yview_scroll(3, "units")
|
||||||
|
|
||||||
canvas.bind_all("<MouseWheel>", _on_mousewheel)
|
canvas.bind_all("<MouseWheel>", _on_mousewheel)
|
||||||
canvas.bind_all("<Button-4>", _on_mousewheel_linux)
|
canvas.bind_all("<Button-4>", _on_mousewheel_linux)
|
||||||
canvas.bind_all("<Button-5>", _on_mousewheel_linux)
|
canvas.bind_all("<Button-5>", _on_mousewheel_linux)
|
||||||
|
|
||||||
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
# --- Scroll pour l'onglet Paramètres ---
|
|
||||||
canvas2 = tk.Canvas(tab_params_outer, bg=CLR_BG, highlightthickness=0)
|
|
||||||
scrollbar2 = ttk.Scrollbar(tab_params_outer, orient=tk.VERTICAL, command=canvas2.yview)
|
|
||||||
self._params_scroll = tk.Frame(canvas2, bg=CLR_BG)
|
|
||||||
self._params_scroll.bind(
|
|
||||||
"<Configure>",
|
|
||||||
lambda e: canvas2.configure(scrollregion=canvas2.bbox("all")),
|
|
||||||
)
|
|
||||||
canvas2_window = canvas2.create_window((0, 0), window=self._params_scroll, anchor="nw")
|
|
||||||
canvas2.configure(yscrollcommand=scrollbar2.set)
|
|
||||||
def _on_canvas2_configure(event):
|
|
||||||
canvas2.itemconfig(canvas2_window, width=event.width)
|
|
||||||
canvas2.bind("<Configure>", _on_canvas2_configure)
|
|
||||||
canvas2.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
||||||
scrollbar2.pack(side=tk.RIGHT, fill=tk.Y)
|
|
||||||
|
|
||||||
# "main" pointe désormais sur le scroll de l'onglet Anonymisation.
|
|
||||||
# Tout le contenu existant (étape 1, formats, boutons, progress, résultats)
|
|
||||||
# reste inchangé — seul le parent implicite a changé.
|
|
||||||
main = self._scroll_frame
|
main = self._scroll_frame
|
||||||
|
pad_x = 32
|
||||||
|
|
||||||
|
# --- Titre ---
|
||||||
|
tk.Label(
|
||||||
|
main, text=APP_TITLE, font=self._f_title,
|
||||||
|
bg=CLR_BG, fg=CLR_TEXT, anchor="w",
|
||||||
|
).pack(fill=tk.X, padx=pad_x, pady=(24, 2))
|
||||||
|
|
||||||
|
tk.Label(
|
||||||
|
main,
|
||||||
|
text="Masquez automatiquement les données personnelles de vos documents PDF.",
|
||||||
|
font=self._f_body, bg=CLR_BG, fg=CLR_TEXT_SECONDARY, anchor="w",
|
||||||
|
).pack(fill=tk.X, padx=pad_x, pady=(0, 18))
|
||||||
|
|
||||||
|
ttk.Separator(main).pack(fill=tk.X, padx=pad_x, pady=(0, 18))
|
||||||
|
|
||||||
# =============================================================
|
# =============================================================
|
||||||
# ÉTAPE 1 — Choix du dossier
|
# ÉTAPE 1 — Choix du dossier
|
||||||
# =============================================================
|
# =============================================================
|
||||||
tk.Label(
|
tk.Label(
|
||||||
main, text="1. Choisir les documents ou fichiers (PDF, Word, Images, Texte)", font=self._f_body_bold,
|
main, text="1. Choisir les documents", font=self._f_body_bold,
|
||||||
bg=CLR_BG, fg=CLR_TEXT, anchor="w",
|
bg=CLR_BG, fg=CLR_TEXT, anchor="w",
|
||||||
).pack(fill=tk.X, padx=pad_x, pady=(0, 6))
|
).pack(fill=tk.X, padx=pad_x, pady=(0, 6))
|
||||||
|
|
||||||
@@ -633,7 +414,7 @@ class App:
|
|||||||
|
|
||||||
self._folder_text_lbl = tk.Label(
|
self._folder_text_lbl = tk.Label(
|
||||||
self._folder_inner,
|
self._folder_inner,
|
||||||
text="Cliquez pour choisir un dossier ou un fichier",
|
text="Cliquez pour choisir un dossier (tous les PDF seront recherchés récursivement)",
|
||||||
font=self._f_body, bg=CLR_CARD_BG, fg=CLR_TEXT_SECONDARY,
|
font=self._f_body, bg=CLR_CARD_BG, fg=CLR_TEXT_SECONDARY,
|
||||||
)
|
)
|
||||||
self._folder_text_lbl.pack(pady=(4, 0))
|
self._folder_text_lbl.pack(pady=(4, 0))
|
||||||
@@ -667,7 +448,7 @@ class App:
|
|||||||
|
|
||||||
tk.Label(
|
tk.Label(
|
||||||
info_inner,
|
info_inner,
|
||||||
text=("\u2022 Recherche récursive de tous les documents dans les sous-dossiers\n"
|
text=("\u2022 Recherche récursive de tous les PDF dans les sous-dossiers\n"
|
||||||
"\u2022 Sortie PDF Image (raster) — sécurité maximale, aucun texte résiduel\n"
|
"\u2022 Sortie PDF Image (raster) — sécurité maximale, aucun texte résiduel\n"
|
||||||
"\u2022 Résultats dans le dossier « anonymise/ » à la racine"),
|
"\u2022 Résultats dans le dossier « anonymise/ » à la racine"),
|
||||||
font=self._f_card_desc, bg=CLR_BLUE_LIGHT, fg=CLR_TEXT_SECONDARY,
|
font=self._f_card_desc, bg=CLR_BLUE_LIGHT, fg=CLR_TEXT_SECONDARY,
|
||||||
@@ -699,7 +480,7 @@ class App:
|
|||||||
buttons_frame.pack(fill=tk.X, padx=pad_x, pady=(0, 4))
|
buttons_frame.pack(fill=tk.X, padx=pad_x, pady=(0, 4))
|
||||||
|
|
||||||
self.btn_run = tk.Button(
|
self.btn_run = tk.Button(
|
||||||
buttons_frame, text="Lancer l'anonymisation",
|
buttons_frame, text="Lancer la pseudonymisation",
|
||||||
font=self._f_button, bg=CLR_PRIMARY, fg="white",
|
font=self._f_button, bg=CLR_PRIMARY, fg="white",
|
||||||
activebackground="#1d4ed8", activeforeground="white",
|
activebackground="#1d4ed8", activeforeground="white",
|
||||||
relief=tk.FLAT, cursor="hand2", pady=10,
|
relief=tk.FLAT, cursor="hand2", pady=10,
|
||||||
@@ -721,94 +502,9 @@ class App:
|
|||||||
main, text="Comment ça marche ?", font=self._f_small,
|
main, text="Comment ça marche ?", font=self._f_small,
|
||||||
bg=CLR_BG, fg=CLR_PRIMARY, cursor="hand2",
|
bg=CLR_BG, fg=CLR_PRIMARY, cursor="hand2",
|
||||||
)
|
)
|
||||||
help_lbl.pack(pady=(0, 8))
|
help_lbl.pack(pady=(0, 18))
|
||||||
help_lbl.bind("<Button-1>", lambda e: self._show_help())
|
help_lbl.bind("<Button-1>", lambda e: self._show_help())
|
||||||
|
|
||||||
# =============================================================
|
|
||||||
# ONGLET "PARAMÈTRES" — contenu monté dans self._params_scroll
|
|
||||||
# =============================================================
|
|
||||||
self._params_frame = self._params_scroll
|
|
||||||
|
|
||||||
tk.Label(
|
|
||||||
self._params_frame,
|
|
||||||
text="Personnaliser le masquage",
|
|
||||||
font=(self._font_family, 14, "bold"),
|
|
||||||
bg=CLR_BG, fg=CLR_TEXT, anchor="w",
|
|
||||||
).pack(fill=tk.X, padx=pad_x, pady=(20, 4))
|
|
||||||
|
|
||||||
tk.Label(
|
|
||||||
self._params_frame,
|
|
||||||
text=("Ces listes complètent les détections automatiques du programme. "
|
|
||||||
"Utile pour gérer les spécificités de votre établissement."),
|
|
||||||
font=self._f_small,
|
|
||||||
bg=CLR_BG, fg=CLR_TEXT_SECONDARY, anchor="w", justify=tk.LEFT, wraplength=700,
|
|
||||||
).pack(fill=tk.X, padx=pad_x, pady=(0, 16))
|
|
||||||
|
|
||||||
# Conteneur interne avec padding latéral pour les listboxes
|
|
||||||
params_inner = tk.Frame(self._params_frame, bg=CLR_BG)
|
|
||||||
params_inner.pack(fill=tk.X, padx=pad_x, pady=(0, 12))
|
|
||||||
|
|
||||||
# --- Whitelist (phrases à ne pas anonymiser) ---
|
|
||||||
self._wl_listbox, self._wl_entry = self._build_phrase_list(
|
|
||||||
params_inner,
|
|
||||||
title="\u2705 Phrases à ne PAS anonymiser :",
|
|
||||||
placeholder="Ajouter une phrase à protéger...",
|
|
||||||
color_tag=CLR_GREEN_LIGHT,
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Blacklist (phrases à toujours masquer) ---
|
|
||||||
self._bl_listbox, self._bl_entry = self._build_phrase_list(
|
|
||||||
params_inner,
|
|
||||||
title="\u26d4 Mots/phrases à TOUJOURS masquer :",
|
|
||||||
placeholder="Ajouter un mot ou phrase à masquer...",
|
|
||||||
color_tag=CLR_PRIMARY_LIGHT,
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Stop-words additionnels (mots à ne jamais identifier comme noms) ---
|
|
||||||
# Différent de la whitelist : agit en amont, pour les sigles, acronymes,
|
|
||||||
# termes métier locaux qui ressemblent à des noms mais n'en sont pas.
|
|
||||||
self._sw_listbox, self._sw_entry = self._build_phrase_list(
|
|
||||||
params_inner,
|
|
||||||
title="\u26a0 Mots à ne jamais identifier comme noms (sigles, acronymes...) :",
|
|
||||||
placeholder="Ajouter un mot (ex: sigle local, acronyme métier)...",
|
|
||||||
color_tag=CLR_ACCENT_LIGHT,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Boutons sauvegarder + exporter
|
|
||||||
btn_row = tk.Frame(params_inner, bg=CLR_BG)
|
|
||||||
btn_row.pack(fill=tk.X, pady=(12, 12))
|
|
||||||
|
|
||||||
export_btn = tk.Button(
|
|
||||||
btn_row, text="\u2709 Exporter pour envoi",
|
|
||||||
font=self._f_small, bg=CLR_ACCENT_LIGHT, fg=CLR_TEXT,
|
|
||||||
relief=tk.GROOVE, cursor="hand2", padx=10, pady=6,
|
|
||||||
command=self._export_params,
|
|
||||||
)
|
|
||||||
export_btn.pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
import_btn = tk.Button(
|
|
||||||
btn_row, text="\u2B07 Importer",
|
|
||||||
font=self._f_small, bg=CLR_PRIMARY_LIGHT, fg=CLR_TEXT,
|
|
||||||
relief=tk.GROOVE, cursor="hand2", padx=10, pady=6,
|
|
||||||
command=self._import_params,
|
|
||||||
)
|
|
||||||
import_btn.pack(side=tk.LEFT, padx=(4, 0))
|
|
||||||
|
|
||||||
save_btn = tk.Button(
|
|
||||||
btn_row, text="Sauvegarder",
|
|
||||||
font=self._f_small, bg=CLR_PRIMARY, fg="white",
|
|
||||||
activebackground=CLR_PRIMARY_DARK, activeforeground="white",
|
|
||||||
relief=tk.FLAT, cursor="hand2", padx=14, pady=6,
|
|
||||||
command=self._save_params,
|
|
||||||
)
|
|
||||||
save_btn.pack(side=tk.RIGHT)
|
|
||||||
|
|
||||||
# Charger les valeurs initiales depuis la config
|
|
||||||
self._load_params()
|
|
||||||
|
|
||||||
# Retour dans l'onglet Anonymisation
|
|
||||||
ttk.Separator(main).pack(fill=tk.X, padx=pad_x, pady=(0, 8))
|
|
||||||
|
|
||||||
# =============================================================
|
# =============================================================
|
||||||
# BARRE DE PROGRESSION (masquée)
|
# BARRE DE PROGRESSION (masquée)
|
||||||
# =============================================================
|
# =============================================================
|
||||||
@@ -915,7 +611,7 @@ class App:
|
|||||||
).pack(side=tk.LEFT)
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
tk.Label(
|
tk.Label(
|
||||||
status_bar, text=_version_long(), font=self._f_small,
|
status_bar, text=APP_VERSION, font=self._f_small,
|
||||||
bg=CLR_BG, fg=CLR_TEXT_SECONDARY, anchor="e",
|
bg=CLR_BG, fg=CLR_TEXT_SECONDARY, anchor="e",
|
||||||
).pack(side=tk.RIGHT)
|
).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
@@ -952,71 +648,22 @@ class App:
|
|||||||
# Actions dossier
|
# Actions dossier
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
def _browse(self):
|
def _browse(self):
|
||||||
"""Propose le choix entre dossier et fichier unique via un menu contextuel."""
|
|
||||||
menu = tk.Menu(self.root, tearoff=0)
|
|
||||||
menu.add_command(label="Choisir un dossier", command=self._browse_folder)
|
|
||||||
menu.add_command(label="Choisir un fichier", command=self._browse_file)
|
|
||||||
# Afficher le menu sous le curseur
|
|
||||||
try:
|
|
||||||
menu.tk_popup(self.root.winfo_pointerx(), self.root.winfo_pointery())
|
|
||||||
finally:
|
|
||||||
menu.grab_release()
|
|
||||||
|
|
||||||
def _browse_folder(self):
|
|
||||||
d = filedialog.askdirectory()
|
d = filedialog.askdirectory()
|
||||||
if d:
|
if d:
|
||||||
self._single_file = None
|
|
||||||
self.dir_var.set(d)
|
self.dir_var.set(d)
|
||||||
self._update_folder_display()
|
self._update_folder_display()
|
||||||
|
|
||||||
def _browse_file(self):
|
|
||||||
try:
|
|
||||||
from format_converter import SUPPORTED_EXTENSIONS
|
|
||||||
except ImportError:
|
|
||||||
SUPPORTED_EXTENSIONS = {".pdf"}
|
|
||||||
# Construire les filtres pour le dialogue
|
|
||||||
ext_list = " ".join(f"*{e}" for e in sorted(SUPPORTED_EXTENSIONS))
|
|
||||||
f = filedialog.askopenfilename(
|
|
||||||
title="Choisir un document à anonymiser",
|
|
||||||
filetypes=[
|
|
||||||
("Documents supportés", ext_list),
|
|
||||||
("PDF", "*.pdf"),
|
|
||||||
("Word", "*.docx"),
|
|
||||||
("Images", "*.jpg *.jpeg *.png *.tiff *.tif *.bmp"),
|
|
||||||
("Texte", "*.txt *.rtf *.odt *.html *.htm"),
|
|
||||||
("Tous", "*.*"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
if f:
|
|
||||||
self._single_file = Path(f)
|
|
||||||
self.dir_var.set(str(self._single_file.parent))
|
|
||||||
self._update_folder_display()
|
|
||||||
|
|
||||||
def _update_folder_display(self):
|
def _update_folder_display(self):
|
||||||
folder = self.dir_var.get()
|
folder = self.dir_var.get()
|
||||||
if not folder:
|
if not folder:
|
||||||
return
|
return
|
||||||
|
|
||||||
is_single = getattr(self, '_single_file', None) is not None
|
# Compter les PDF (récursif)
|
||||||
|
pdf_count = 0
|
||||||
if is_single:
|
try:
|
||||||
doc_count = 1
|
pdf_count = len([p for p in Path(folder).rglob("*.pdf") if p.is_file()])
|
||||||
display_label = self._single_file.name
|
except Exception:
|
||||||
else:
|
pass
|
||||||
# Compter les documents supportés (récursif)
|
|
||||||
try:
|
|
||||||
from format_converter import SUPPORTED_EXTENSIONS
|
|
||||||
except ImportError:
|
|
||||||
SUPPORTED_EXTENSIONS = {".pdf"}
|
|
||||||
doc_count = 0
|
|
||||||
try:
|
|
||||||
doc_count = len([
|
|
||||||
p for p in Path(folder).rglob("*")
|
|
||||||
if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS
|
|
||||||
])
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
display_label = folder
|
|
||||||
|
|
||||||
# Vider et reconstruire l'intérieur
|
# Vider et reconstruire l'intérieur
|
||||||
for w in self._folder_inner.winfo_children():
|
for w in self._folder_inner.winfo_children():
|
||||||
@@ -1025,9 +672,8 @@ class App:
|
|||||||
row = tk.Frame(self._folder_inner, bg=CLR_CARD_BG)
|
row = tk.Frame(self._folder_inner, bg=CLR_CARD_BG)
|
||||||
row.pack(fill=tk.X)
|
row.pack(fill=tk.X)
|
||||||
|
|
||||||
icon = "\U0001f4c4" if is_single else "\U0001f4c2" # 📄 ou 📂
|
|
||||||
tk.Label(
|
tk.Label(
|
||||||
row, text=icon, font=(self._font_family, 16),
|
row, text="\U0001f4c2", font=(self._font_family, 16),
|
||||||
bg=CLR_CARD_BG,
|
bg=CLR_CARD_BG,
|
||||||
).pack(side=tk.LEFT, padx=(0, 8))
|
).pack(side=tk.LEFT, padx=(0, 8))
|
||||||
|
|
||||||
@@ -1035,7 +681,7 @@ class App:
|
|||||||
info_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
info_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||||
|
|
||||||
# Chemin (tronqué si trop long)
|
# Chemin (tronqué si trop long)
|
||||||
display_path = display_label
|
display_path = folder
|
||||||
if len(display_path) > 60:
|
if len(display_path) > 60:
|
||||||
display_path = "..." + display_path[-57:]
|
display_path = "..." + display_path[-57:]
|
||||||
tk.Label(
|
tk.Label(
|
||||||
@@ -1043,13 +689,9 @@ class App:
|
|||||||
bg=CLR_CARD_BG, fg=CLR_TEXT, anchor="w",
|
bg=CLR_CARD_BG, fg=CLR_TEXT, anchor="w",
|
||||||
).pack(fill=tk.X)
|
).pack(fill=tk.X)
|
||||||
|
|
||||||
if is_single:
|
suffix = "PDF trouvé (récursif)" if pdf_count <= 1 else "PDF trouvés (récursif)"
|
||||||
subtitle = f"Fichier unique — {self._single_file.suffix.upper().lstrip('.')}"
|
|
||||||
else:
|
|
||||||
suffix = "document trouvé (récursif)" if doc_count <= 1 else "documents trouvés (récursif)"
|
|
||||||
subtitle = f"{doc_count} {suffix}"
|
|
||||||
tk.Label(
|
tk.Label(
|
||||||
info_frame, text=subtitle,
|
info_frame, text=f"{pdf_count} {suffix}",
|
||||||
font=self._f_small, bg=CLR_CARD_BG, fg=CLR_TEXT_SECONDARY, anchor="w",
|
font=self._f_small, bg=CLR_CARD_BG, fg=CLR_TEXT_SECONDARY, anchor="w",
|
||||||
).pack(fill=tk.X)
|
).pack(fill=tk.X)
|
||||||
|
|
||||||
@@ -1067,41 +709,21 @@ class App:
|
|||||||
# Lancement
|
# Lancement
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
def _run(self):
|
def _run(self):
|
||||||
is_single = getattr(self, '_single_file', None) is not None
|
folder = Path(self.dir_var.get().strip())
|
||||||
|
if not folder.is_dir():
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Dossier invalide",
|
||||||
|
"Choisissez un dossier contenant des PDF.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if is_single:
|
pdfs = sorted([p for p in folder.rglob("*.pdf") if p.is_file()])
|
||||||
# Mode fichier unique
|
if not pdfs:
|
||||||
if not self._single_file.is_file():
|
messagebox.showwarning(
|
||||||
messagebox.showwarning("Fichier introuvable", f"{self._single_file}")
|
"Aucun PDF",
|
||||||
return
|
"Aucun fichier PDF trouvé\n(recherche récursive dans les sous-dossiers).",
|
||||||
folder = self._single_file.parent
|
)
|
||||||
pdfs = [self._single_file]
|
return
|
||||||
else:
|
|
||||||
# Mode dossier
|
|
||||||
folder = Path(self.dir_var.get().strip())
|
|
||||||
if not folder.is_dir():
|
|
||||||
messagebox.showwarning(
|
|
||||||
"Dossier invalide",
|
|
||||||
"Choisissez un dossier ou un fichier.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
from format_converter import SUPPORTED_EXTENSIONS
|
|
||||||
except ImportError:
|
|
||||||
SUPPORTED_EXTENSIONS = {".pdf"}
|
|
||||||
pdfs = sorted([
|
|
||||||
p for p in folder.rglob("*")
|
|
||||||
if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS
|
|
||||||
])
|
|
||||||
if not pdfs:
|
|
||||||
exts = ", ".join(sorted(SUPPORTED_EXTENSIONS))
|
|
||||||
messagebox.showwarning(
|
|
||||||
"Aucun document",
|
|
||||||
f"Aucun fichier supporté trouvé.\n"
|
|
||||||
f"Formats acceptés : {exts}\n"
|
|
||||||
f"(recherche récursive dans les sous-dossiers)",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._stop_requested = False
|
self._stop_requested = False
|
||||||
self.btn_run.pack_forget()
|
self.btn_run.pack_forget()
|
||||||
@@ -1157,12 +779,8 @@ class App:
|
|||||||
and self._vlm_manager.is_loaded()
|
and self._vlm_manager.is_loaded()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Utiliser process_document (multi-formats) si disponible,
|
outputs = core.process_pdf(
|
||||||
# sinon fallback sur process_pdf (PDF uniquement)
|
pdf_path=pdf,
|
||||||
_process_fn = getattr(core, 'process_document', None) or core.process_pdf
|
|
||||||
_path_key = "doc_path" if _process_fn.__name__ == "process_document" else "pdf_path"
|
|
||||||
outputs = _process_fn(
|
|
||||||
**{_path_key: pdf},
|
|
||||||
out_dir=outdir,
|
out_dir=outdir,
|
||||||
make_vector_redaction=False,
|
make_vector_redaction=False,
|
||||||
also_make_raster_burn=True,
|
also_make_raster_burn=True,
|
||||||
@@ -1344,248 +962,6 @@ class App:
|
|||||||
"« anonymise/ » à la racine du dossier sélectionné.",
|
"« anonymise/ » à la racine du dossier sélectionné.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
# Paramètres avancés (whitelist/blacklist)
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
def _build_phrase_list(self, parent, title: str, placeholder: str, color_tag: str):
|
|
||||||
"""Construit un widget liste + ajout/suppression pour les phrases."""
|
|
||||||
frame = tk.Frame(parent, bg=CLR_BG)
|
|
||||||
frame.pack(fill=tk.X, pady=(4, 8))
|
|
||||||
|
|
||||||
tk.Label(
|
|
||||||
frame, text=title, font=self._f_small,
|
|
||||||
bg=CLR_BG, fg=CLR_TEXT, anchor="w",
|
|
||||||
).pack(fill=tk.X, pady=(0, 4))
|
|
||||||
|
|
||||||
# Zone de saisie + bouton ajouter
|
|
||||||
input_row = tk.Frame(frame, bg=CLR_BG)
|
|
||||||
input_row.pack(fill=tk.X, pady=(0, 4))
|
|
||||||
|
|
||||||
entry = tk.Entry(input_row, font=self._f_small, relief=tk.GROOVE, bd=1)
|
|
||||||
entry.insert(0, placeholder)
|
|
||||||
entry.configure(fg="#999")
|
|
||||||
|
|
||||||
def _on_focus_in(e):
|
|
||||||
if entry.get() == placeholder:
|
|
||||||
entry.delete(0, tk.END)
|
|
||||||
entry.configure(fg=CLR_TEXT)
|
|
||||||
|
|
||||||
def _on_focus_out(e):
|
|
||||||
if not entry.get().strip():
|
|
||||||
entry.insert(0, placeholder)
|
|
||||||
entry.configure(fg="#999")
|
|
||||||
|
|
||||||
entry.bind("<FocusIn>", _on_focus_in)
|
|
||||||
entry.bind("<FocusOut>", _on_focus_out)
|
|
||||||
entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 4))
|
|
||||||
|
|
||||||
def _add(event=None):
|
|
||||||
text = entry.get().strip()
|
|
||||||
if text and text != placeholder:
|
|
||||||
# Éviter les doublons
|
|
||||||
items = list(listbox.get(0, tk.END))
|
|
||||||
if text not in items:
|
|
||||||
listbox.insert(tk.END, text)
|
|
||||||
entry.delete(0, tk.END)
|
|
||||||
|
|
||||||
add_btn = tk.Button(
|
|
||||||
input_row, text="+ Ajouter", font=self._f_small,
|
|
||||||
bg=color_tag, fg=CLR_TEXT, relief=tk.GROOVE, cursor="hand2",
|
|
||||||
command=_add, padx=8,
|
|
||||||
)
|
|
||||||
add_btn.pack(side=tk.LEFT)
|
|
||||||
entry.bind("<Return>", _add)
|
|
||||||
|
|
||||||
# Liste des phrases
|
|
||||||
list_frame = tk.Frame(frame, bg=CLR_BG)
|
|
||||||
list_frame.pack(fill=tk.X)
|
|
||||||
|
|
||||||
listbox = tk.Listbox(
|
|
||||||
list_frame, height=4, font=("Consolas", 9),
|
|
||||||
relief=tk.GROOVE, bd=1, selectmode=tk.EXTENDED,
|
|
||||||
bg=color_tag,
|
|
||||||
)
|
|
||||||
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=listbox.yview)
|
|
||||||
listbox.configure(yscrollcommand=scrollbar.set)
|
|
||||||
listbox.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
||||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
||||||
|
|
||||||
# Bouton supprimer
|
|
||||||
def _remove():
|
|
||||||
sel = listbox.curselection()
|
|
||||||
for idx in reversed(sel):
|
|
||||||
listbox.delete(idx)
|
|
||||||
|
|
||||||
rm_btn = tk.Button(
|
|
||||||
frame, text="Supprimer la sélection", font=self._f_small,
|
|
||||||
bg="#ffcdd2", fg="#b71c1c", relief=tk.GROOVE, cursor="hand2",
|
|
||||||
command=_remove, padx=8,
|
|
||||||
)
|
|
||||||
rm_btn.pack(anchor="e", pady=(2, 0))
|
|
||||||
|
|
||||||
return listbox, entry
|
|
||||||
|
|
||||||
def _load_params(self):
|
|
||||||
"""Charge les whitelist/blacklist depuis la config YAML."""
|
|
||||||
try:
|
|
||||||
cfg_path = Path(self.cfg_path.get())
|
|
||||||
if cfg_path.exists() and yaml is not None:
|
|
||||||
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
|
|
||||||
# Whitelist
|
|
||||||
wl = data.get("whitelist_phrases", [])
|
|
||||||
self._wl_listbox.delete(0, tk.END)
|
|
||||||
for phrase in wl:
|
|
||||||
if phrase and phrase.strip():
|
|
||||||
self._wl_listbox.insert(tk.END, phrase.strip())
|
|
||||||
# Blacklist
|
|
||||||
bl = data.get("blacklist", {}).get("force_mask_terms", [])
|
|
||||||
self._bl_listbox.delete(0, tk.END)
|
|
||||||
for term in bl:
|
|
||||||
if term and str(term).strip():
|
|
||||||
self._bl_listbox.insert(tk.END, str(term).strip())
|
|
||||||
# Stop-words additionnels
|
|
||||||
sw = data.get("additional_stopwords", [])
|
|
||||||
self._sw_listbox.delete(0, tk.END)
|
|
||||||
for term in sw:
|
|
||||||
if term and str(term).strip():
|
|
||||||
self._sw_listbox.insert(tk.END, str(term).strip())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _export_params(self):
|
|
||||||
"""Exporte les paramètres whitelist/blacklist dans un fichier JSON pour envoi par email."""
|
|
||||||
try:
|
|
||||||
import json as _json
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
wl = list(self._wl_listbox.get(0, tk.END))
|
|
||||||
bl = list(self._bl_listbox.get(0, tk.END))
|
|
||||||
sw = list(self._sw_listbox.get(0, tk.END))
|
|
||||||
|
|
||||||
export_data = {
|
|
||||||
"version": APP_VERSION,
|
|
||||||
"date_export": datetime.now().isoformat(),
|
|
||||||
"etablissement": "", # à remplir par l'utilisateur
|
|
||||||
"whitelist_phrases": wl,
|
|
||||||
"blacklist_force_mask_terms": bl,
|
|
||||||
"additional_stopwords": sw,
|
|
||||||
"instructions": (
|
|
||||||
"Ce fichier contient les paramètres d'anonymisation personnalisés. "
|
|
||||||
"Envoyez-le par email à l'équipe technique pour mise à jour du programme."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Proposer le Bureau comme destination par défaut
|
|
||||||
desktop = Path.home() / "Desktop"
|
|
||||||
if not desktop.exists():
|
|
||||||
desktop = Path.home() / "Bureau"
|
|
||||||
if not desktop.exists():
|
|
||||||
desktop = Path.home()
|
|
||||||
|
|
||||||
dest = filedialog.asksaveasfilename(
|
|
||||||
title="Exporter les paramètres",
|
|
||||||
initialdir=str(desktop),
|
|
||||||
initialfile="parametres_anonymisation.json",
|
|
||||||
defaultextension=".json",
|
|
||||||
filetypes=[("JSON", "*.json"), ("Tous", "*.*")],
|
|
||||||
)
|
|
||||||
if dest:
|
|
||||||
Path(dest).write_text(
|
|
||||||
_json.dumps(export_data, ensure_ascii=False, indent=2),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
messagebox.showinfo(
|
|
||||||
"Export réussi",
|
|
||||||
f"Paramètres exportés dans :\n{dest}\n\n"
|
|
||||||
f"Vous pouvez envoyer ce fichier par email\n"
|
|
||||||
f"à l'équipe technique.",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
messagebox.showerror("Erreur", f"Erreur à l'export :\n{e}")
|
|
||||||
|
|
||||||
def _import_params(self):
|
|
||||||
"""Importe des paramètres depuis un fichier JSON (fusionne avec l'existant)."""
|
|
||||||
try:
|
|
||||||
import json as _json
|
|
||||||
|
|
||||||
src = filedialog.askopenfilename(
|
|
||||||
title="Importer des paramètres",
|
|
||||||
filetypes=[("JSON", "*.json"), ("Tous", "*.*")],
|
|
||||||
)
|
|
||||||
if not src:
|
|
||||||
return
|
|
||||||
|
|
||||||
data = _json.loads(Path(src).read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
# Fusionner whitelist
|
|
||||||
new_wl = data.get("whitelist_phrases", [])
|
|
||||||
existing_wl = set(self._wl_listbox.get(0, tk.END))
|
|
||||||
added_wl = 0
|
|
||||||
for phrase in new_wl:
|
|
||||||
if phrase and phrase.strip() and phrase.strip() not in existing_wl:
|
|
||||||
self._wl_listbox.insert(tk.END, phrase.strip())
|
|
||||||
added_wl += 1
|
|
||||||
|
|
||||||
# Fusionner blacklist
|
|
||||||
new_bl = data.get("blacklist_force_mask_terms", [])
|
|
||||||
existing_bl = set(self._bl_listbox.get(0, tk.END))
|
|
||||||
added_bl = 0
|
|
||||||
for term in new_bl:
|
|
||||||
if term and str(term).strip() and str(term).strip() not in existing_bl:
|
|
||||||
self._bl_listbox.insert(tk.END, str(term).strip())
|
|
||||||
added_bl += 1
|
|
||||||
|
|
||||||
# Fusionner stop-words additionnels
|
|
||||||
new_sw = data.get("additional_stopwords", [])
|
|
||||||
existing_sw = set(self._sw_listbox.get(0, tk.END))
|
|
||||||
added_sw = 0
|
|
||||||
for term in new_sw:
|
|
||||||
if term and str(term).strip() and str(term).strip() not in existing_sw:
|
|
||||||
self._sw_listbox.insert(tk.END, str(term).strip())
|
|
||||||
added_sw += 1
|
|
||||||
|
|
||||||
version = data.get("version", "?")
|
|
||||||
date_exp = data.get("date_export", "?")[:10]
|
|
||||||
messagebox.showinfo(
|
|
||||||
"Import réussi",
|
|
||||||
f"Paramètres importés (v{version}, {date_exp}) :\n\n"
|
|
||||||
f" + {added_wl} phrase(s) ajoutée(s) à la whitelist\n"
|
|
||||||
f" + {added_bl} terme(s) ajouté(s) à la blacklist\n"
|
|
||||||
f" + {added_sw} mot(s) ajouté(s) aux stop-words\n\n"
|
|
||||||
f"Cliquez sur « Sauvegarder » pour appliquer.",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
messagebox.showerror("Erreur", f"Erreur à l'import :\n{e}")
|
|
||||||
|
|
||||||
def _save_params(self):
|
|
||||||
"""Sauvegarde les whitelist/blacklist dans la config YAML."""
|
|
||||||
try:
|
|
||||||
cfg_path = Path(self.cfg_path.get())
|
|
||||||
if not cfg_path.exists() or yaml is None:
|
|
||||||
messagebox.showwarning("Erreur", "Fichier de configuration introuvable.")
|
|
||||||
return
|
|
||||||
|
|
||||||
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
|
|
||||||
|
|
||||||
# Whitelist phrases
|
|
||||||
data["whitelist_phrases"] = list(self._wl_listbox.get(0, tk.END))
|
|
||||||
|
|
||||||
# Blacklist terms
|
|
||||||
if "blacklist" not in data:
|
|
||||||
data["blacklist"] = {}
|
|
||||||
data["blacklist"]["force_mask_terms"] = list(self._bl_listbox.get(0, tk.END))
|
|
||||||
|
|
||||||
# Stop-words additionnels (mots à ne jamais identifier comme noms)
|
|
||||||
data["additional_stopwords"] = list(self._sw_listbox.get(0, tk.END))
|
|
||||||
|
|
||||||
cfg_path.write_text(
|
|
||||||
yaml.dump(data, allow_unicode=True, default_flow_style=False, sort_keys=False),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
messagebox.showinfo("Paramètres", "Paramètres sauvegardés avec succès.")
|
|
||||||
except Exception as e:
|
|
||||||
messagebox.showerror("Erreur", f"Impossible de sauvegarder :\n{e}")
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
# YAML (interne)
|
# YAML (interne)
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
|
|||||||
20
ano/pdf_natif/pseudonymise/FC14.audit.jsonl
Normal file
20
ano/pdf_natif/pseudonymise/FC14.audit.jsonl
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{"page": 0, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 0, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 1, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 1, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 2, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 2, "kind": "OGC_court", "original": "N° OGC : 14", "placeholder": "[OGC]", "bbox_hint": null}
|
||||||
|
{"page": 2, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 3, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 3, "kind": "OGC_court", "original": "N° OGC : 14", "placeholder": "[OGC]", "bbox_hint": null}
|
||||||
|
{"page": 3, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 0, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 0, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 1, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 1, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 2, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 2, "kind": "OGC_court", "original": "N° OGC : 14", "placeholder": "[OGC]", "bbox_hint": null}
|
||||||
|
{"page": 2, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
|
{"page": 3, "kind": "FINESS", "original": "640780417", "placeholder": "[FINESS]", "bbox_hint": null}
|
||||||
|
{"page": 3, "kind": "OGC_court", "original": "N° OGC : 14", "placeholder": "[OGC]", "bbox_hint": null}
|
||||||
|
{"page": 3, "kind": "force_term", "original": "CENTRE HOSPITALIER COTE BASQUE", "placeholder": "[MASK]", "bbox_hint": null}
|
||||||
348
ano/pdf_natif/pseudonymise/FC14.pseudonymise.txt
Normal file
348
ano/pdf_natif/pseudonymise/FC14.pseudonymise.txt
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
NNNN°°°° OOOOGGGGCCCC : ::: 11114444
|
||||||
|
FICHE MEDICALE DE RECUEIL DU PRATICIEN CONSEIL (une fiche par RUM)
|
||||||
|
Seul le recodage impactant la facturation est renseigné
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] Date début contrôle : 12/05/2025
|
||||||
|
N° champ : 1 Libellé champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Dossier manquant : 0 Dates du séjour : 09/05/2023 au 10/05/2023
|
||||||
|
Données du
|
||||||
|
séjour
|
||||||
|
)sna(
|
||||||
|
egA
|
||||||
|
)sruoj(
|
||||||
|
egA exeS
|
||||||
|
.nred
|
||||||
|
ialéD selgèr egA
|
||||||
|
noitatseg
|
||||||
|
sdioP
|
||||||
|
eértne'd ed
|
||||||
|
eéruD ruojés edoM
|
||||||
|
eértne'd
|
||||||
|
ecnanevorP
|
||||||
|
edoM
|
||||||
|
eitros
|
||||||
|
ed
|
||||||
|
noitanitseD secnaés
|
||||||
|
bN
|
||||||
|
MUR
|
||||||
|
bN
|
||||||
|
HXE
|
||||||
|
j bN
|
||||||
|
BXE
|
||||||
|
epyT
|
||||||
|
BXE
|
||||||
|
j bN
|
||||||
|
Etablissement 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Recodage 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Données du RUM Nature Nb
|
||||||
|
Lits dédiés SP UM IGS II Durée RUM
|
||||||
|
suppl. suppl.
|
||||||
|
0
|
||||||
|
N° RUM Etablissement : 1/2 0 29 C 0 0 0
|
||||||
|
du 09/05/2023 au 09/05/2023
|
||||||
|
0
|
||||||
|
N° RUM Recodage : 1/2 0 29 C 0 0 0
|
||||||
|
du 09/05/2023 au 09/05/2023
|
||||||
|
Codage de l’Etablissement Recodage
|
||||||
|
DP K851 PANCREATITE AIG. BIL. K801
|
||||||
|
DR
|
||||||
|
DAS
|
||||||
|
Actes
|
||||||
|
Rappel : un code CIM de DAS suivi d’un astérisque correspond à une CMA exclue par le DP
|
||||||
|
GHM établissement : 07C131 GHS établissement : 2347 GHM après recodage : 07C141 GHS après recodage : 2351
|
||||||
|
Praticien conseil Médecin DIM
|
||||||
|
Recodage impactant la facturation : 1 Accord
|
||||||
|
GHS injustifié : 0 SE FFM FSD Désaccord
|
||||||
|
En fonction des DP/DR et actes retenus par le PC, seul le recodage d'une des CMA les plus élevées ayant une incidence sur la racine GHM
|
||||||
|
ou sur la facturation des suppléments sera renseigné. Hors RSS injustifié avec actes externes, seuls les actes classants seront recodés
|
||||||
|
|
||||||
|
11114444
|
||||||
|
N° OGC :
|
||||||
|
FICHE MEDICALE DE RECUEIL DU PRATICIEN CONSEIL (une fiche par RUM)
|
||||||
|
Seul le recodage impactant la facturation est renseigné
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] Date début contrôle : 12/05/2025
|
||||||
|
N° champ : 1 Libellé champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Dossier manquant : 0 Dates du séjour : 09/05/2023 au 10/05/2023
|
||||||
|
Données du
|
||||||
|
séjour
|
||||||
|
)sna(
|
||||||
|
egA
|
||||||
|
)sruoj(
|
||||||
|
egA exeS
|
||||||
|
.nred
|
||||||
|
ialéD selgèr egA
|
||||||
|
noitatseg
|
||||||
|
sdioP
|
||||||
|
eértne'd ed
|
||||||
|
eéruD ruojés edoM
|
||||||
|
eértne'd
|
||||||
|
ecnanevorP
|
||||||
|
edoM
|
||||||
|
eitros
|
||||||
|
ed
|
||||||
|
noitanitseD secnaés
|
||||||
|
bN
|
||||||
|
MUR
|
||||||
|
bN
|
||||||
|
HXE
|
||||||
|
j bN
|
||||||
|
BXE
|
||||||
|
epyT
|
||||||
|
BXE
|
||||||
|
j bN
|
||||||
|
Etablissement 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Recodage 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Données du RUM Nature Nb
|
||||||
|
Lits dédiés SP UM IGS II Durée RUM
|
||||||
|
suppl. suppl.
|
||||||
|
1
|
||||||
|
N° RUM Etablissement : 2/2 0 53 C 0 0 0
|
||||||
|
du 09/05/2023 au 10/05/2023
|
||||||
|
1
|
||||||
|
N° RUM Recodage : 2/2 0 53 C 0 0 0
|
||||||
|
du 09/05/2023 au 10/05/2023
|
||||||
|
Codage de l’Etablissement Recodage
|
||||||
|
DP K851 PANCREATITE AIG. BIL. K801
|
||||||
|
DR
|
||||||
|
DAS
|
||||||
|
HMFC004 1 CHOLÉCYSTECTOMIE COELIO. HMFC004 1
|
||||||
|
HMFC004 4 CHOLÉCYSTECTOMIE COELIO. HMFC004 4
|
||||||
|
Actes
|
||||||
|
Rappel : un code CIM de DAS suivi d’un astérisque correspond à une CMA exclue par le DP
|
||||||
|
GHM établissement : 07C131 GHS établissement : 2347 GHM après recodage : 07C141 GHS après recodage : 2351
|
||||||
|
Praticien conseil Médecin DIM
|
||||||
|
Recodage impactant la facturation : 1 Accord
|
||||||
|
GHS injustifié : 0 SE FFM FSD Désaccord
|
||||||
|
En fonction des DP/DR et actes retenus par le PC, seul le recodage d'une des CMA les plus élevées ayant une incidence sur la racine GHM
|
||||||
|
ou sur la facturation des suppléments sera renseigné. Hors RSS injustifié avec actes externes, seuls les actes classants seront recodés
|
||||||
|
|
||||||
|
FICHE MEDICALE DE CONCERTATION
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] [OGC]
|
||||||
|
N° Champ : 1 Libellé du champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Document couvert par le secret médical
|
||||||
|
Ne peut pas être produit aux services administratifs de l’établissement et des organismes de sécurité sociale
|
||||||
|
Nom du praticien-conseil : V VAILLENDET Nom du médecin du DIM :
|
||||||
|
Homme de 61 ans
|
||||||
|
Antécédent :
|
||||||
|
Pancréatite aiguë d'origine indéterminée d'évolution favorable.
|
||||||
|
Hospitalisation du 9 au 10/5/23
|
||||||
|
Admis à distance de l’épisode de pancréatite pour une
|
||||||
|
cholécystectomie par laparoscopie
|
||||||
|
En peropératoire présence de calculs intra-vésiculaires => en
|
||||||
|
faveur d'une origine lithiasique de cette PA.
|
||||||
|
La cholangiographie peropératoire ne retrouvait pas de calcul
|
||||||
|
dans la VBP.
|
||||||
|
Codage DP :
|
||||||
|
Cholécystectomie « à froid » suite à une pancréatite
|
||||||
|
Le CRO mentionne une légère inflammation séquellaire de la
|
||||||
|
pancréatite.
|
||||||
|
Il n’y a donc pas de pancréatite aigüe sur ce séjour, c’est un
|
||||||
|
antécédent
|
||||||
|
Pas de notion de cholécystite aigue
|
||||||
|
Codage retenu : K80.1 « Calcul de la vésicule biliaire avec une
|
||||||
|
autre forme de cholécystite »
|
||||||
|
Nb : 2 RUM
|
||||||
|
Probable changement d’unité après chirurgie => même codage
|
||||||
|
pour els 2 RUM
|
||||||
|
Date de concertation :
|
||||||
|
NOM et SIGNATURE du MEDECIN RESPONSABLE du CONTRÔLE NOM et SIGNATURE du MEDECIN du DIM
|
||||||
|
Dr Gilles DE MONREDON Atteste avoir pris connaissance des éléments du dossier y compris
|
||||||
|
ceux couverts par le secret médical et des arguments soutenus par
|
||||||
|
les médecins contrôleurs et avoir eu l’opportunité d’en débattre
|
||||||
|
contradictoirement
|
||||||
|
NOM du ou des autres participants à la concertation
|
||||||
|
NOM du ou des autres membres de l’équipe de contrôle ayant
|
||||||
|
participé à la concertation
|
||||||
|
|
||||||
|
FICHE ADMINISTRATIVE DE CONCERTATION 1/2
|
||||||
|
(à établir lors de la concertation avec le médecin du DIM)
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] [OGC]
|
||||||
|
N° Champ : 1 Libellé du champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Document susceptible d’être produit aux services administratifs de l’établissement et des organismes de sécurité sociale,
|
||||||
|
n’inscrire aucun élément couvert par le secret médical.
|
||||||
|
ARGUMENTAIRE DU MEDECIN CONTROLEUR
|
||||||
|
142 : La facturation du GHS par l’établissement n’est pas conforme à l’article 1 de l’arrêté du 19 février 2015 modifié du fait d’un non-
|
||||||
|
respect des règles de codage édictées dans l’annexe II de l’arrêté du 23 décembre 2016 modifié. En préalable, chapitre VI, paragraphe
|
||||||
|
1.2 : « Les circonstances du diagnostic préalable n’importent pas (…) La situation de traitement est présente lorsque le diagnostic de
|
||||||
|
l’affection est fait au moment de l’entrée du patient dans l’unité médicale et que l’admission a pour but le traitement de l’affection. »
|
||||||
|
Le non-respect des règles porte sur le diagnostic principal (DP) codé par l’établissement dans le résumé d’unité médicale (RUM). Le DP
|
||||||
|
n’est pas conforme aux règles de codage des diagnostics rappelées par l’annexe II, chapitre VI, paragraphe 1.2.2.1 : « Dans la situation
|
||||||
|
de traitement unique chirurgical, le DP est en général la maladie opérée [Règle T3]. (…) Le diagnostic résultant de l’intervention peut
|
||||||
|
être différent du diagnostic préopératoire (…). Le DP doit en effet être énoncé en connaissance de l’ensemble des informations
|
||||||
|
acquises au cours du séjour. » Au vu des éléments présents dans le dossier du patient, alors que l’admission a été motivée par le
|
||||||
|
traitement chirurgical d’une affection, l’établissement n’a pas retenu le code de cette affection en DP.
|
||||||
|
|
||||||
|
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] Date début contrôle : 12/05/2025
|
||||||
|
N° champ : 1 Libellé champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Données du
|
||||||
|
séjour : )sna(
|
||||||
|
egA )sruoj(
|
||||||
|
egA exeS .nred
|
||||||
|
selgèr
|
||||||
|
ialéD noitatseg
|
||||||
|
egA eértne'd
|
||||||
|
sdioP ed
|
||||||
|
eéruD ruojés eértne'd
|
||||||
|
edoM ecnanevorP eitros
|
||||||
|
edoM
|
||||||
|
ed noitanitseD secnaés
|
||||||
|
bN MUR
|
||||||
|
bN HXE
|
||||||
|
j
|
||||||
|
bN BXE
|
||||||
|
epyT BXE
|
||||||
|
j
|
||||||
|
bN
|
||||||
|
Données du
|
||||||
|
séjour
|
||||||
|
Etablissement : 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Recodage : 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Données du RUM : Lits dédiés SP UM IGS II Durée RUM Nature
|
||||||
|
suppl. Nb
|
||||||
|
suppl.
|
||||||
|
N° RUM Etablissement : 1/2 0 29 C 0 0 0 0
|
||||||
|
du 09/05/2023 au 09/05/2023
|
||||||
|
N° RUM Recodage : 1/2 0 29 C 0 0 0 0
|
||||||
|
du 09/05/2023 au 09/05/2023
|
||||||
|
Codage de l’Etablissement : Recodage
|
||||||
|
DP : K851 PANCREATITE AIG. BIL. K801
|
||||||
|
DR
|
||||||
|
DAS
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Actes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
GHM établissement : 07C131 GHS établissement : 2347 GHM après recodage : 07C141 GHS après recodage : 2351
|
||||||
|
Praticien conseil : Médecin DIM
|
||||||
|
Recodage impactant la facturation : 1
|
||||||
|
GHS injustifié : 0 SE FFM FSD
|
||||||
|
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] Date début contrôle : 12/05/2025
|
||||||
|
N° champ : 1 Libellé champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Données du
|
||||||
|
séjour : )sna(
|
||||||
|
egA )sruoj(
|
||||||
|
egA exeS .nred
|
||||||
|
selgèr
|
||||||
|
ialéD noitatseg
|
||||||
|
egA eértne'd
|
||||||
|
sdioP ed
|
||||||
|
eéruD ruojés eértne'd
|
||||||
|
edoM ecnanevorP eitros
|
||||||
|
edoM
|
||||||
|
ed noitanitseD secnaés
|
||||||
|
bN MUR
|
||||||
|
bN HXE
|
||||||
|
j
|
||||||
|
bN BXE
|
||||||
|
epyT BXE
|
||||||
|
j
|
||||||
|
bN
|
||||||
|
Données du
|
||||||
|
séjour
|
||||||
|
Etablissement : 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Recodage : 61 1 0 1 8 8 0 2 0 0 0
|
||||||
|
Données du RUM : Lits dédiés SP UM IGS II Durée RUM Nature
|
||||||
|
suppl. Nb
|
||||||
|
suppl.
|
||||||
|
N° RUM Etablissement : 2/2 0 53 C 0 1 0 0
|
||||||
|
du 09/05/2023 au 10/05/2023
|
||||||
|
N° RUM Recodage : 2/2 0 53 C 0 1 0 0
|
||||||
|
du 09/05/2023 au 10/05/2023
|
||||||
|
Codage de l’Etablissement : Recodage
|
||||||
|
DP : K851 PANCREATITE AIG. BIL. K801
|
||||||
|
DR
|
||||||
|
DAS
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Actes : HMFC004 1 CHOLÉCYSTECTOMIE COELIO. HMFC004 1
|
||||||
|
HMFC004 : 4 CHOLÉCYSTECTOMIE COELIO. HMFC004 4
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
GHM établissement : 07C131 GHS établissement : 2347 GHM après recodage : 07C141 GHS après recodage : 2351
|
||||||
|
Praticien conseil : Médecin DIM
|
||||||
|
Recodage impactant la facturation : 1
|
||||||
|
GHS injustifié : 0 SE FFM FSD
|
||||||
|
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] [OGC]
|
||||||
|
N° Champ : 1 Libellé du champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Document couvert par le secret médical
|
||||||
|
Ne peut pas être produit aux services administratifs de l’établissement et des organismes de sécurité sociale
|
||||||
|
Nom du praticien-conseil : V VAILLENDET Nom du médecin du DIM :
|
||||||
|
Homme de 61 ans
|
||||||
|
Antécédent : Pancréatite aiguë d'origine indéterminée d'évolution favorable.
|
||||||
|
Hospitalisation du 9 au 10/5/23
|
||||||
|
Admis à distance de l’épisode de pancréatite pour une
|
||||||
|
cholécystectomie par laparoscopie
|
||||||
|
En peropératoire présence de calculs intra-vésiculaires => en
|
||||||
|
faveur d'une origine lithiasique de cette PA.
|
||||||
|
La cholangiographie peropératoire ne retrouvait pas de calcul
|
||||||
|
dans la VBP.
|
||||||
|
Codage DP :
|
||||||
|
Cholécystectomie « à froid » suite à une pancréatite
|
||||||
|
Le CRO mentionne une légère inflammation séquellaire de la
|
||||||
|
pancréatite.
|
||||||
|
Il n’y a donc pas de pancréatite aigüe sur ce séjour, c’est un
|
||||||
|
antécédent
|
||||||
|
Pas de notion de cholécystite aigue
|
||||||
|
Codage retenu : K80.1 « Calcul de la vésicule biliaire avec une
|
||||||
|
autre forme de cholécystite »
|
||||||
|
Nb : 2 RUM
|
||||||
|
Probable changement d’unité après chirurgie => même codage
|
||||||
|
pour els 2 RUM
|
||||||
|
NOM et SIGNATURE du MEDECIN RESPONSABLE du CONTRÔLE
|
||||||
|
Dr Gilles DE MONREDON
|
||||||
|
NOM du ou des autres membres de l’équipe de contrôle ayant
|
||||||
|
participé à la concertation : NOM et SIGNATURE du MEDECIN du DIM
|
||||||
|
Atteste avoir pris connaissance des éléments du dossier y compris
|
||||||
|
ceux couverts par le secret médical et des arguments soutenus par
|
||||||
|
les médecins contrôleurs et avoir eu l’opportunité d’en débattre
|
||||||
|
contradictoirement
|
||||||
|
NOM du ou des autres participants à la concertation
|
||||||
|
|
||||||
|
Etablissement : [MASK] FINESS : [FINESS] [OGC]
|
||||||
|
N° Champ : 1 Libellé du champ de contrôle : Séjours correspondant à la racine 07C13
|
||||||
|
Document susceptible d’être produit aux services administratifs de l’établissement et des organismes de sécurité sociale,
|
||||||
|
n’inscrire aucun élément couvert par le secret médical.
|
||||||
|
ARGUMENTAIRE DU MEDECIN CONTROLEUR
|
||||||
|
142 : La facturation du GHS par l’établissement n’est pas conforme à l’article 1 de l’arrêté du 19 février 2015 modifié du fait d’un non-
|
||||||
|
respect des règles de codage édictées dans l’annexe II de l’arrêté du 23 décembre 2016 modifié. En préalable, chapitre VI, paragraphe
|
||||||
|
1.2 : « Les circonstances du diagnostic préalable n’importent pas (…) La situation de traitement est présente lorsque le diagnostic de
|
||||||
|
l’affection est fait au moment de l’entrée du patient dans l’unité médicale et que l’admission a pour but le traitement de l’affection. »
|
||||||
|
Le non-respect des règles porte sur le diagnostic principal (DP) codé par l’établissement dans le résumé d’unité médicale (RUM). Le DP
|
||||||
|
n’est pas conforme aux règles de codage des diagnostics rappelées par l’annexe II, chapitre VI, paragraphe 1.2.2.1 : « Dans la situation
|
||||||
|
de traitement unique chirurgical, le DP est en général la maladie opérée [Règle T3]. (…) Le diagnostic résultant de l’intervention peut
|
||||||
|
être différent du diagnostic préopératoire (…). Le DP doit en effet être énoncé en connaissance de l’ensemble des informations
|
||||||
|
acquises au cours du séjour. » Au vu des éléments présents dans le dossier du patient, alors que l’admission a été motivée par le
|
||||||
|
traitement chirurgical d’une affection, l’établissement n’a pas retenu le code de cette affection en DP.
|
||||||
|
|
||||||
|
|
||||||
|
| ||||||