- 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>
123 lines
4.0 KiB
Python
123 lines
4.0 KiB
Python
"""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
|