feat: cache Ollama + parallélisation ThreadPool + filtrage DAS renforcé + modules GHM/CPAM/export RUM
- Cache persistant JSON thread-safe pour les résultats Ollama (invalidation par modèle) - Parallélisation des appels Ollama (ThreadPoolExecutor, 2 workers) - 6 nouvelles règles de filtrage DAS parasites (doublons, ponctuation, OCR, labo, fragments) - Client Ollama centralisé (mode JSON natif + retry) - Module GHM (estimation CMD/sévérité) - Module contrôle CPAM (parser + contre-argumentation RAG) - Export RUM (format RSS) - Viewer enrichi (détail dossier) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
0
src/export/__init__.py
Normal file
0
src/export/__init__.py
Normal file
190
src/export/rum_export.py
Normal file
190
src/export/rum_export.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""Export au format RUM (Résumé d'Unité Médicale) V016 pour le groupeur ATIH.
|
||||
|
||||
Génère une ligne fixe de 165 caractères suivie de zones variables
|
||||
(DAS en 8 chars, actes CCAM en 29 chars chacun).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from ..config import FINESS, NUM_UM, DossierMedical
|
||||
|
||||
|
||||
@dataclass
|
||||
class RUMConfig:
|
||||
finess: str = FINESS
|
||||
num_um: str = NUM_UM
|
||||
|
||||
|
||||
def _format_cim10(code: str | None) -> str:
|
||||
"""Formate un code CIM-10 sur 8 caractères (sans point, padded)."""
|
||||
if not code:
|
||||
return " " * 8
|
||||
clean = code.upper().replace(".", "").strip()
|
||||
return clean.ljust(8)[:8]
|
||||
|
||||
|
||||
def _format_date(date_str: str | None) -> str:
|
||||
"""Convertit une date DD/MM/YYYY ou YYYY-MM-DD en DDMMYYYY (8 chars)."""
|
||||
if not date_str:
|
||||
return " " * 8
|
||||
date_str = date_str.strip()
|
||||
# Format DD/MM/YYYY
|
||||
m = re.match(r"(\d{2})/(\d{2})/(\d{4})", date_str)
|
||||
if m:
|
||||
return f"{m.group(1)}{m.group(2)}{m.group(3)}"
|
||||
# Format YYYY-MM-DD
|
||||
m = re.match(r"(\d{4})-(\d{2})-(\d{2})", date_str)
|
||||
if m:
|
||||
return f"{m.group(3)}{m.group(2)}{m.group(1)}"
|
||||
return " " * 8
|
||||
|
||||
|
||||
def _format_sex(sexe: str | None) -> str:
|
||||
"""Convertit le sexe en code RUM (1=M, 2=F)."""
|
||||
if not sexe:
|
||||
return " "
|
||||
s = sexe.strip().upper()
|
||||
if s in ("M", "MASCULIN", "HOMME", "H"):
|
||||
return "1"
|
||||
if s in ("F", "FEMININ", "FÉMININ", "FEMME"):
|
||||
return "2"
|
||||
return " "
|
||||
|
||||
|
||||
def _map_mode_entree(text: str | None) -> str:
|
||||
"""Convertit le mode d'entrée textuel en code RUM (1 char)."""
|
||||
if not text:
|
||||
return " "
|
||||
t = text.strip().lower()
|
||||
mapping = {
|
||||
"domicile": "8",
|
||||
"mutation": "6",
|
||||
"transfert": "7",
|
||||
"urgences": "8",
|
||||
"urgence": "8",
|
||||
}
|
||||
for key, code in mapping.items():
|
||||
if key in t:
|
||||
return code
|
||||
return " "
|
||||
|
||||
|
||||
def _map_mode_sortie(text: str | None) -> str:
|
||||
"""Convertit le mode de sortie textuel en code RUM (1 char)."""
|
||||
if not text:
|
||||
return " "
|
||||
t = text.strip().lower()
|
||||
mapping = {
|
||||
"domicile": "8",
|
||||
"mutation": "6",
|
||||
"transfert": "7",
|
||||
"deces": "9",
|
||||
"décès": "9",
|
||||
"décédé": "9",
|
||||
"decede": "9",
|
||||
}
|
||||
for key, code in mapping.items():
|
||||
if key in t:
|
||||
return code
|
||||
return " "
|
||||
|
||||
|
||||
def _format_ccam_act(acte) -> str:
|
||||
"""Formate un acte CCAM sur 29 caractères.
|
||||
|
||||
Structure : code(7) + phase(1) + activité(1) + date(8) + doc/extension(12)
|
||||
"""
|
||||
code = (acte.code_ccam_suggestion or "").upper().replace(" ", "")
|
||||
code = code.ljust(7)[:7]
|
||||
phase = "1"
|
||||
activite = "1"
|
||||
date = _format_date(acte.date)
|
||||
extension = " " * 12
|
||||
return f"{code}{phase}{activite}{date}{extension}"
|
||||
|
||||
|
||||
def export_rum(dossier: DossierMedical, config: RUMConfig | None = None) -> str:
|
||||
"""Génère le texte RUM complet pour un dossier médical.
|
||||
|
||||
Returns:
|
||||
Chaîne texte au format RUM V016 (165 chars fixes + zones variables).
|
||||
"""
|
||||
if config is None:
|
||||
config = RUMConfig()
|
||||
|
||||
sejour = dossier.sejour
|
||||
dp = dossier.diagnostic_principal
|
||||
|
||||
# Compteurs
|
||||
das_list = dossier.diagnostics_associes
|
||||
actes_list = dossier.actes_ccam
|
||||
nb_das = len(das_list)
|
||||
nb_actes = len(actes_list)
|
||||
|
||||
# Numéros générés
|
||||
source = dossier.source_file or "UNKNOWN"
|
||||
num_rss = source.replace(".pdf", "").replace(" ", "_").ljust(20)[:20]
|
||||
num_admin = num_rss
|
||||
num_rum = source[:10].ljust(10)[:10]
|
||||
|
||||
# Construction de la zone fixe (165 caractères)
|
||||
parts = [
|
||||
" " * 2, # 1-2 : Version classification (vide)
|
||||
" " * 6, # 3-8 : GHM (vide, rempli par groupeur)
|
||||
" ", # 9 : Filler
|
||||
"016", # 10-12 : Version format
|
||||
" " * 3, # 13-15 : Code retour
|
||||
config.finess.ljust(9)[:9], # 16-24 : FINESS
|
||||
"016", # 25-27 : Version RUM
|
||||
num_rss, # 28-47 : N° RSS
|
||||
num_admin, # 48-67 : N° admin
|
||||
num_rum, # 68-77 : N° RUM
|
||||
_format_date(None), # 78-85 : Date naissance (non disponible)
|
||||
_format_sex(sejour.sexe), # 86 : Sexe
|
||||
config.num_um.ljust(4)[:4], # 87-90 : N° UM
|
||||
" " * 2, # 91-92 : Type autorisation
|
||||
_format_date(sejour.date_entree), # 93-100: Date entrée UM
|
||||
_map_mode_entree(sejour.mode_entree), # 101 : Mode entrée
|
||||
" ", # 102 : Provenance
|
||||
_format_date(sejour.date_sortie), # 103-110: Date sortie UM
|
||||
_map_mode_sortie(sejour.mode_sortie), # 111 : Mode sortie
|
||||
" ", # 112 : Destination
|
||||
" " * 5, # 113-117: CP résidence
|
||||
" " * 4, # 118-121: Poids nné
|
||||
" " * 2, # 122-123: Âge gestationnel
|
||||
"00", # 124-125: Nb séances
|
||||
str(nb_das).zfill(2)[-2:], # 126-127: Nb DAS
|
||||
"00", # 128-129: Nb DAD
|
||||
str(nb_actes).zfill(2)[-2:], # 130-131: Nb actes
|
||||
_format_cim10(dp.cim10_suggestion if dp else None), # 132-139: DP
|
||||
" " * 8, # 140-147: DR
|
||||
" " * 3, # 148-150: IGS2
|
||||
" " * 15, # 151-165: Réservé
|
||||
]
|
||||
|
||||
fixed = "".join(parts)
|
||||
assert len(fixed) == 165, f"Zone fixe RUM: attendu 165, obtenu {len(fixed)}"
|
||||
|
||||
# Zones variables
|
||||
variable_parts: list[str] = []
|
||||
|
||||
# DAS (8 chars chacun)
|
||||
for das in das_list:
|
||||
variable_parts.append(_format_cim10(das.cim10_suggestion))
|
||||
|
||||
# Actes CCAM (29 chars chacun)
|
||||
for acte in actes_list:
|
||||
variable_parts.append(_format_ccam_act(acte))
|
||||
|
||||
return fixed + "".join(variable_parts)
|
||||
|
||||
|
||||
def save_rum(dossier: DossierMedical, path: Path, config: RUMConfig | None = None) -> None:
|
||||
"""Exporte un dossier au format RUM dans un fichier."""
|
||||
rum_text = export_rum(dossier, config)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(rum_text, encoding="utf-8")
|
||||
Reference in New Issue
Block a user