Files
oto-enterprise-os-dtp/05_deliverables_mvp/crm/commissions/commlib/finance.py
T
Claude Code DTP Worker e7cc3c4a3c [DTP-Worker 20260812_080104] fix correctness · commissions vendeurs — la formule traçable base×taux cassait sur montants réels
Bug latent réel (crm/commissions/commlib/finance.py:75) : le libellé de base de
la formule traçable « commission = base × taux » utilisait f"{base:g}", qui casse
deux fois sur des montants immobiliers réels en RD (une unité USD 300k ≈ 18M DOP) :
(1) notation exponentielle dès 1e6 (18000000 → 1.8e+07, illisible/non auditable) ;
(2) arrondi silencieux à 6 chiffres significatifs (123456.78 → 123457) = une base
FABRIQUÉE ≠ de la réelle, l'invention interdite par #6 dans le module même qui
proclame l'anti-invention.

Fix : helper _amount_label (f"{x:f}" jamais exponentiel + strip zéros) → décimal
fidèle, identique à l'ancien pour tous les montants simples (200000 reste 200000) ;
seuls les cas buggés changent. 2 tests à dents (millions non-exponentiel + décimales
préservées) — teeth prouvé : les DEUX échouent sans le fix.

Byte-repro : 0 impact d'artefact du module (compute_line est runtime, jamais appelé
par le générateur). Cascade compteur-de-suite seule : matrice 631/614 → 633/616,
regression×3 + quality_report régénérés, fiches qa/erpnext/crm + README réalignés
(commissions 25→27, Total CRM 81→83). run_ci 33/0/0. 0 code moteur V18 (bloqué D-06).
0 gate ajouté (#5). 0 commande VPS (#8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 08:11:32 +00:00

117 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)."""
if not is_filled(value):
return "{taux_pct}"
if isinstance(value, (int, float)) and not isinstance(value, bool):
return f"{value:g} %"
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"]]