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:
dom
2026-02-11 12:43:34 +01:00
parent 7e69f994b0
commit 9d07894c6f
12 changed files with 1013 additions and 26 deletions

View 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