# Business rules Memo racine pour les regles metier transverses ou multi-modules. Pour les regles propres a un module, preferer la documentation locale quand elle existe, par exemple: - `modules/purchase_trade/docs/business-rules.md` - `modules/purchase_trade/docs/padding-invoice-accounting.md` ## Session 2026-04-28 - Devises, TVA, padding et lots ### `account.move.line` / saisie manuelle en seconde devise - `rate` est un vrai champ stocke et editable, plus seulement un champ de visualisation. - Le sens du taux suit l'usage Tryton: taux devise seconde / devise societe. Donc, avec une devise societe a 1, le montant societe se calcule par `amount_second_currency / rate`. - Une ligne est consideree comme manuelle si elle n'a ni `origin`, ni `move_origin`, ni `move.origin`. - En manuel, si l'utilisateur saisit d'abord `amount_second_currency`, le systeme reprend le dernier taux disponible pour la devise seconde a la date de la ligne, puis calcule `debit` ou `credit`. - Si l'utilisateur veut forcer le taux, il efface d'abord `debit` / `credit`, saisit `rate`, et le montant societe est recalcule depuis le montant en devise seconde. - Si le montant societe et le montant devise seconde sont deja presents, le taux implicite est `abs(amount_second_currency) / abs(base_amount)`. - Les lignes generees automatiquement depuis facture doivent aussi renseigner `rate` quand `amount_second_currency` existe. - Un fallback central sur `account.move.line.create` calcule le taux manquant depuis les montants pour eviter les chemins oublies. - Comme `rate` devient stocke, une mise a jour du module est necessaire; les anciennes lignes doivent etre regenerees ou backfillees si on veut afficher le taux historique. ### Causes racines des bugs devises - Deux `on_change_amount_second_currency` existaient; le second ecrasait la logique de conversion. - La branche negative testait deux fois `> 0`, donc un montant devise seconde negatif ne generait pas correctement le credit. - Le taux initial etait calcule a l'envers par rapport au standard Tryton attendu. - Les records transitoires Tryton peuvent ne pas exposer un champ non assigne; les chemins generiques doivent utiliser `getattr(record, field, None)`. ### `account.invoice` / taux et devise - `on_change_with_rate` depend explicitement de `currency`, `company`, `invoice_date`, `selection_rate`, `rate_date` et `lines`. - Ne pas declarer `lines.amount` dans `@fields.depends`: ce champ relationnel n'existe pas cote definition de vue et provoque un `KeyError`. - Les move lines de facture, lignes produit, lignes stock/drop et lignes TVA doivent renseigner `rate` des qu'elles portent `amount_second_currency`. ### TVA - Les taxes sur `invoice.lines[].taxes` alimentent les taxes calculees de la facture via les lignes taxables. - Les taxes globales de facture (`invoice.taxes`) peuvent etre automatiques ou manuelles. - Les taxes manuelles sont preservees par `update_taxes`; les taxes automatiques sont synchronisees, supprimees ou recreees selon les lignes. - Une ligne facture genere des `account.tax.line` de type `base`. - Une ligne de taxe facture genere une move line comptable avec `account.tax.line` de type `tax`; si la taxe est manuelle, une ligne `base` complementaire peut etre ajoutee. - La seconde devise est portee par `account.move.line` (`second_currency` / `amount_second_currency`), pas par `account.tax.line` qui suit la devise comptable de la move line. ### Padding - Les chemins padding doivent verifier que `lot` existe avant de lire `sale_invoice_line_prov` ou `sale_invoice_line`. - Le bug observe n'etait pas la logique padding elle-meme mais un acces direct a un lot absent sur certaines lignes de facture. ### Payment term - Le calcul des echeances doit fonctionner meme si la premiere ligne facture n'a pas d'`origin`. - `term_lines` doit etre calcule hors de la branche qui depend du modele d'origine, avec une ligne metier optionnelle. ### Periodes comptables - `Period.find` utilise un cache par societe/date/etat. - Une periode creee ou ouverte peut continuer a etre vue comme absente par un worker tant que le cache ou le serveur n'a pas ete rafraichi. ### `lot.qt` / validation sale et purchase - Cote `sale.line`, la creation de la ligne cree deja le lot virtuel et sa quantite. - Le passage initial de `quantity_theorical` de `None` a la quantite de la ligne ne doit pas etre traite comme un delta physique; sinon `lot.qt` est double. - Le correctif sale ignore le delta quand l'ancienne valeur theorique est `None`; les modifications suivantes restent bien appliquees en delta. - Cote `purchase.line`, la logique etait deja protegee par une baseline sur la quantite courante du lot virtuel quand l'ancienne valeur est `None`. ### Valuation / PNL achat - Une purchase line peut ne pas produire de valorisation si elle est finie, sans lot, filtree par `valuation_type`, sans `lot_price` pour les modes `priced` / `efp`, sans fees relies a des lots, ou sans derivatives. - En mode `basis`, sans composant, le fallback metier continue de passer par la ligne summary quand elle existe. ## Session 2026-04-29 - Purchase trade: rate fee et PnL lots ouverts ### `fee.fee` / mode `% rate` - Le calcul du montant d'un fee en mode `rate` est aligne entre purchase et sale. - Il ne depend pas du `payment_term`. - Il ne depend pas de la date du jour ni de `fee_date`. - La periode de financement est directement `fin_int_delta` sur la ligne `Estimated date` avec `trigger = bldate`. - La formule reste en interet simple ACT/360: `amount = rate_factor * unit_price * quantity`, avec `rate_factor = (price / 100) * fin_int_delta / 360`. - Si aucune Estimated Date `bldate` n'est renseignee, le fee `% rate` ne calcule pas de montant. ### Valuation / PnL avec lots ouverts matches - Tant que les lots physiques ne sont pas crees, une purchase et une sale peuvent etre matchees uniquement via `lot.qt`: `lot_p` purchase ouvert -> `lot_s` sale ouvert. - Dans ce cas, les lignes PnL purchase-side (`Pur. price`, `Pur. fee`) doivent aussi renseigner `sale` et `sale_line` pour apparaitre dans l'onglet PnL de la sale matchee. - Quand le lot physique existe, `lot.sale_line` reste le chemin prioritaire. - Si un lot purchase ouvert est matche a plusieurs sales differentes, ne pas attacher arbitrairement une seule sale aux lignes purchase-side. - `Mark as finished` ne veut pas dire "ne plus calculer le PnL": il ignore seulement le reliquat ouvert/virtuel; les lots physiques, fees physiques et derivatives continuent d'etre valorises. ## Session 2026-04-30 - PnL fees ouverts et `% rate` ### PnL fees sur lots ouverts - Un lot ouvert / virtuel avec quantite courante a zero ne doit plus generer de lignes de fees PnL. - Cette regle suit la meme intention que `Mark as finished`: le reliquat ouvert/virtuel ignore ne doit pas porter de PnL, y compris pour les fees. - Les lots physiques restent valorisables; le filtre vise seulement les lots non physiques vides. - Le cas important est celui des fees `rate`, `ppack` ou `lumpsum`, dont le montant peut rester non nul meme quand la quantite affichee du lot est zero. ### Fee `% rate` - `fin_int_delta` est la periode absolue de calcul du financement. - Le montant ne depend pas de `Date.today()`, de `fee_date`, ni d'un intervalle entre la date du jour et `BL date + delta`. - Purchase et sale appliquent la meme formule ACT/360: `amount = unit_price * quantity * (price / 100) * fin_int_delta / 360`. - La ligne `Estimated date` avec `trigger = bldate` reste le support metier pour porter `fin_int_delta`; la date estimee elle-meme ne sert pas au calcul du montant `% rate`. - Le montant affiche sur `fee.fee` reste non signe, meme si `fin_int_delta` est negatif. Le PnL applique seul le signe metier: `PAY` negatif, `REC` positif. ## Session 2026-05-01 - Solde ouvert apres lots physiques ### `purchase.line` / `sale.line` et `lot.qt` - Une modification de `quantity_theorical` ne doit plus ajuster le `lot.qt` libre uniquement par delta. - Le solde ouvert est recalcule en tenant compte des lots physiques deja crees: `quantity_theorical - somme(lots physiques)`. - Le `lot.qt` libre non matche / non shippe est ensuite aligne sur ce solde moins les `lot.qt` deja matches ou shippes. - Si ce calcul donne un solde negatif, la modification est bloquee avec `Please unlink or unmatch lot`. - Cas de reference: une ligne achat passee de `10000` a `20000` avec deja `10000` physiques doit rester a `10000` ouverts et `10000` physiques. ### `fee.fee` / `fee.lots` - Le lien entre un fee et son lot virtuel est conserve comme fallback. - Des qu'un fee possede au moins un lot physique dans `fee.lots`, les lots physiques deviennent la base effective de `fee.quantity`. - Une hausse purement ouverte de `quantity_theorical` n'impacte donc pas un fee deja porte par des lots physiques. - En `ppack`, `fee.quantity` suit la somme des `lot_qt` physiques; dans les autres modes quantitatifs, elle suit la somme des quantites courantes des lots physiques. - A la suppression du dernier physique, le fee retombe sur son lot virtuel. - Le PnL fee suit la meme logique: full open tant que seul le virtuel existe, puis uniquement les lots physiques effectifs. ## Session 2026-05-06 - Remove physical lot ### `lot.remove` / lot physique shippe ou matche - L'action `Remove physical lot` ne bloque plus automatiquement un lot physique shippe ou matche. - Si le lot est shippe, matche, ou les deux, l'utilisateur doit confirmer un warning avant suppression. - La suppression reste interdite si le `stock.move` lie au lot physique n'est pas en etat `draft`, car le mouvement peut deja etre engage ou comptabilise. - Quand la suppression est confirmee, le `lot.move`, le `stock.move` draft et le lot physique sont supprimes. - La quantite du lot physique est restauree dans `lot.qt` avec le meme contexte metier: - meme lot purchase virtuel (`lot_p`); - meme shipment si le lot etait shippe; - meme lot sale virtuel (`lot_s`) si le lot etait matche. - Si une ligne `lot.qt` existe deja avec ce meme contexte, la quantite est agregee sur la ligne existante au lieu de creer un doublon.