cb8069b83c
Explore very-thorough sur crm/dossier_vente · crm/financement_bancaire · faisabilite/bancable · frontend/chat_otoia · pie/manifest · qa/audit_4big · mobile/app_config · fiscal/ecf_dgii → 0 défaut réel. Les 4 candidats remontés sont tous NON-défauts (verify-non-defects #6) : 3× `:g` sur domaine BORNÉ (nombre d'unités / pourcentages 0-100 → exponentiel + arrondi-6-chiffres hors domaine, et `:g` nettoie le bruit flottant) + 1× HTML chat_otoia à entrée canonique contrôlée. Preuve robuste : report.py `_money` réserve sciemment `,.0f`/`,.2f` aux montants non bornés → les `:g` restants sont un choix d'ingénierie correct, pas un oubli. Fait nouveau : le taux commissions `:g` (déjà statué non-défaut en 080104) a RÉCIDIVÉ de flag ce cycle, et bancable ×2 fraîchement flaggés → la classe gaspille un cycle d'audit à chaque passage. `grep ':g}'` prod = exactement 3 sites. Passe unique : note de justification in-situ à chacun (domaine borné, 2 pièges hors domaine, montants → `,.0f`, « ne pas corriger »). Convertit le faux-positif récurrent en « déjà revu, sûr ». 0 fix de code (défauts inexistants ; corriger `:g`→`:f` réintroduirait du bruit flottant = régression) · 0 gate (#5) · 0 artefact reconstruit (bancable sans out/ ; _rate_label runtime non sérialisé) · matrice inchangée 633/616/17 · run_ci 33/0/0 · NFC-clean · 0 code moteur V18 (bloqué D-06) · 0 commande VPS. M crm/commissions/commlib/finance.py M faisabilite/bancable/banclib/finance.py M faisabilite/bancable/banclib/report.py M 05_activity_log/2026-08-12.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
4.7 KiB
Python
123 lines
4.7 KiB
Python
"""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
|
||
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"]]
|