"""Calcul TRAÇABLE des commissions vendeurs (anti-invention · CLAUDE.md #6). Même doctrine que `banclib/finance.py` : un calcul transparent, reproductible et entièrement sourcé n'est PAS une invention — c'est de la modélisation auditable. Chaque ligne de commission publie sa FORMULE avec sa valeur : montant = base × taux_pct où `base` provient du Dossier Vente (un champ Currency réel, ex. montant_contrat) et `taux_pct` provient du barème (fourni par la Direction AVEC sa source). Si l'un des deux opérandes manque (`null` / placeholder), la valeur reste `None` (placeholder, jamais 0-inventé) et la ligne est marquée `incomplete` — la formule reste affichée. Aucun taux n'est jamais fabriqué : le barème livré porte `taux_pct: null` tant que la Direction ne l'a pas confirmé. """ from __future__ import annotations from typing import Any, Optional from . import deps is_filled = deps.is_filled def _num(value: Any) -> Optional[float]: """Valeur numérique réelle, ou None si absente/placeholder/non numérique.""" if not is_filled(value): return None try: return float(value) except (TypeError, ValueError): return None def rate(value: Any) -> Optional[float]: """Taux de commission → fraction. Accepte 3.5 (nombre) ou « 3.5 % » (texte). Retourne None si absent/placeholder — jamais un taux par défaut fabriqué. """ if not is_filled(value): return None if isinstance(value, bool): return None if isinstance(value, (int, float)): return float(value) / 100.0 # `replace(",", ".")` accepte la virgule décimale française (« 3,5 % » → 3.5). # Un séparateur de milliers (« 1.234,56 ») casserait ce parse — mais il est # INATTEIGNABLE : un taux de commission est borné 0-100 (cf. doctrine de # `_rate_label`), donc jamais ≥ 1000 → aucun séparateur de milliers. La virgule # est donc toujours décimale ici. Ne pas « corriger » vers locale.atof (#5/#6). txt = str(value).replace("%", "").replace(",", ".").strip() try: return float(txt) / 100.0 except ValueError: return None def _amount_label(x: float) -> str: """Montant affiché dans la formule : décimal FIDÈLE. `:g` (l'ancien encodeur) cassait deux fois sur des montants réels : (1) il bascule en notation exponentielle dès 1e6 (« 18000000 » → « 1.8e+07 » — illisible dans une formule censée être auditable) et (2) il arrondit à 6 chiffres significatifs (« 123456.78 » → « 123457 ») ce qui FABRIQUE une base différente de la réelle — exactement l'invention interdite par CLAUDE.md #6. `:f` (jamais exponentiel) puis strip des zéros/point superflus donne un décimal fidèle et lisible, identique à l'ancien pour les montants simples (« 200000 » reste « 200000 »).""" s = f"{x:f}" # décimal complet, jamais de notation exponentielle if "." in s: s = s.rstrip("0").rstrip(".") return s def _rate_label(value: Any) -> str: """Libellé du taux tel qu'affiché dans la formule (verbatim si texte). NB : `:g` est VOLONTAIRE et sûr ici (≠ montants → `_amount_label`). Un taux de commission est borné (0-100), donc jamais en notation exponentielle (< 1e6) et jamais au-delà de 6 chiffres significatifs : les deux pièges de `:g` documentés sur `_amount_label` sont inatteignables sur ce domaine, et `:g` nettoie en prime le bruit flottant (« 3.0 » → « 3 »). Ne pas « corriger » en `:f` (audit récurrent).""" if not is_filled(value): return "{taux_pct}" if isinstance(value, (int, float)) and not isinstance(value, bool): return f"{value:g} %" # borné 0-100 : sûr, cf. docstring return str(value).strip() def compute_line(dossier: dict, event: dict) -> dict: """Une ligne de commission traçable pour un évènement du barème. `dossier` : instance (partielle) d'un OTO Dossier Vente (base + devise). `event` : un évènement du barème (update_value, role_id, base_field, taux). """ base_field = event["base_field"] base = _num(dossier.get(base_field)) taux = rate(event.get("taux_pct")) devise = dossier.get("devise") montant = base * taux if (base is not None and taux is not None) else None base_lbl = _amount_label(base) if base is not None else f"{{{base_field}}}" formule = f"{base_lbl} × {_rate_label(event.get('taux_pct'))}" manquants: list[str] = [] if base is None: manquants.append(base_field) if taux is None: manquants.append("taux_pct") return { "update_value": event["update_value"], "role_id": event["role_id"], "base_field": base_field, "base": base, "taux_pct": taux, "devise": devise if is_filled(devise) else None, "montant": montant, "formule": formule, "incomplete": bool(manquants), "champs_manquants": manquants, } def compute_dossier(dossier: dict, bareme: dict) -> list[dict]: """Toutes les lignes de commission d'un dossier (ordre = ordre du barème).""" return [compute_line(dossier, ev) for ev in bareme["evenements"]]