feat: Phase 4 — viewer enrichi, non-cumul CCAM, fusion multi-PDFs + rebuild FAISS (21 141 vecteurs)
- Viewer : badges compteurs (DAS, actes, alertes, CMA), raisonnement LLM pliable, regroupement CCAM, navigation patient, alertes NON-CUMUL en rouge - Non-cumul CCAM : 3 règles heuristiques (même base, même regroupement/jour, paires incompatibles) - Fusion multi-PDFs : merge_dossiers() avec priorité Trackare, spécificité CIM-10, déduplication, champ source_files - Index FAISS reconstruit : 21 141 vecteurs (CCAM dict 8 257 + CIM-10 alpha 306) - 192 tests unitaires passent Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
122
src/medical/ccam_noncumul.py
Normal file
122
src/medical/ccam_noncumul.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""Détection des incompatibilités de non-cumul entre actes CCAM.
|
||||
|
||||
Implémente 3 règles heuristiques basées sur les principes T2A :
|
||||
1. Même code de base (7 caractères) avec activités différentes
|
||||
2. Même regroupement chirurgical le même jour
|
||||
3. Paires de regroupements incompatibles connues
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..config import ActeCCAM
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Regroupements chirurgicaux soumis à cumul restreint (un seul par jour)
|
||||
REGROUPEMENT_UNIQUE_PAR_JOUR: set[str] = {
|
||||
"ADC", # Actes de chirurgie
|
||||
"ACO", # Actes de chirurgie orthopédique
|
||||
"ADO", # Actes de chirurgie ORL
|
||||
"ADA", # Actes de chirurgie abdominale/digestive
|
||||
"ADE", # Actes de chirurgie endoscopique
|
||||
}
|
||||
|
||||
# Paires de regroupements incompatibles
|
||||
NONCUMUL_REGROUPEMENT_PAIRS: set[frozenset[str]] = {
|
||||
frozenset({"ADC", "ADE"}),
|
||||
frozenset({"ADC", "ADO"}),
|
||||
frozenset({"ACO", "ADE"}),
|
||||
}
|
||||
|
||||
|
||||
def _get_regroupement(acte: ActeCCAM) -> str | None:
|
||||
"""Récupère le regroupement d'un acte depuis le dictionnaire CCAM."""
|
||||
if not acte.code_ccam_suggestion:
|
||||
return None
|
||||
try:
|
||||
from .ccam_dict import load_dict
|
||||
d = load_dict()
|
||||
info = d.get(acte.code_ccam_suggestion)
|
||||
if info and isinstance(info, dict):
|
||||
return info.get("regroupement")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def check_noncumul(actes: list[ActeCCAM]) -> list[str]:
|
||||
"""Vérifie les règles de non-cumul entre actes CCAM.
|
||||
|
||||
Args:
|
||||
actes: Liste d'actes CCAM d'un dossier médical.
|
||||
|
||||
Returns:
|
||||
Liste d'alertes de non-cumul détectées.
|
||||
"""
|
||||
if len(actes) < 2:
|
||||
return []
|
||||
|
||||
alertes: list[str] = []
|
||||
|
||||
# Enrichir les actes avec leur regroupement
|
||||
actes_info: list[tuple[ActeCCAM, str | None]] = [
|
||||
(acte, _get_regroupement(acte)) for acte in actes
|
||||
]
|
||||
|
||||
# Règle 1 : même code de base (7 premiers caractères), activités différentes
|
||||
codes_base: dict[str, list[ActeCCAM]] = {}
|
||||
for acte in actes:
|
||||
code = acte.code_ccam_suggestion
|
||||
if code and len(code) >= 7:
|
||||
base = code[:7]
|
||||
codes_base.setdefault(base, []).append(acte)
|
||||
|
||||
for base, group in codes_base.items():
|
||||
if len(group) > 1:
|
||||
codes_full = [a.code_ccam_suggestion for a in group]
|
||||
alertes.append(
|
||||
f"NON-CUMUL: codes de même base {base} avec variantes "
|
||||
f"({', '.join(codes_full)}) — vérifier la facturation"
|
||||
)
|
||||
|
||||
# Règle 2 : même regroupement chirurgical le même jour
|
||||
regroup_par_jour: dict[tuple[str, str | None], list[ActeCCAM]] = {}
|
||||
for acte, regroup in actes_info:
|
||||
if regroup and regroup in REGROUPEMENT_UNIQUE_PAR_JOUR:
|
||||
key = (regroup, acte.date)
|
||||
regroup_par_jour.setdefault(key, []).append(acte)
|
||||
|
||||
for (regroup, date), group in regroup_par_jour.items():
|
||||
if len(group) > 1:
|
||||
codes = [a.code_ccam_suggestion or "?" for a in group]
|
||||
jour = f" le {date}" if date else ""
|
||||
alertes.append(
|
||||
f"NON-CUMUL: {len(group)} actes du regroupement {regroup}{jour} "
|
||||
f"({', '.join(codes)}) — cumul restreint"
|
||||
)
|
||||
|
||||
# Règle 3 : paires de regroupements incompatibles
|
||||
regroups_seen: list[tuple[str, ActeCCAM]] = [
|
||||
(r, a) for a, r in actes_info if r
|
||||
]
|
||||
checked: set[frozenset[int]] = set()
|
||||
for i, (r1, a1) in enumerate(regroups_seen):
|
||||
for j, (r2, a2) in enumerate(regroups_seen):
|
||||
if i >= j:
|
||||
continue
|
||||
pair_key = frozenset({i, j})
|
||||
if pair_key in checked:
|
||||
continue
|
||||
checked.add(pair_key)
|
||||
pair = frozenset({r1, r2})
|
||||
if pair in NONCUMUL_REGROUPEMENT_PAIRS:
|
||||
alertes.append(
|
||||
f"NON-CUMUL: regroupements incompatibles {r1}/{r2} "
|
||||
f"({a1.code_ccam_suggestion or '?'} + {a2.code_ccam_suggestion or '?'})"
|
||||
)
|
||||
|
||||
return alertes
|
||||
Reference in New Issue
Block a user