# Business Rules - Purchase Trade Statut: `draft` Version: `v0.8` Derniere mise a jour: `2026-05-10` Owner metier: `a completer` Owner technique: `a completer` ## 1) Scope - Domaine: `purchase_trade` - Hors scope: - Modules impactes: - `purchase_trade` - `lot` ## 2) Glossaire - `Purchase Line`: ligne d'achat. - `quantity_theorical`: quantite theorique contractuelle de la ligne. - `Virtual Lot`: lot unique de type `virtual` rattache a une `purchase.line`. - `lot.qt`: table des quantites ouvertes, matchées ou shippées par lot. - `lot.qt ouvert`: enregistrement `lot.qt` avec `lot_p = virtual lot`, `lot_s = None` et sans shipment. ## 3) Regles metier ### BR-PT-001 - Ajustement de la quantite theorique apres creation du contrat - Intent: conserver la coherence entre la quantite theorique de la ligne d'achat, le lot virtuel associe et les quantites ouvertes stockees dans `lot.qt`. - Description: - Quand `purchase.line.quantity_theorical` est modifiee apres creation du contrat, le systeme doit recalculer le delta entre l'ancienne et la nouvelle valeur. - La regle s'applique au lot unique de type `virtual` rattache a la `purchase.line`. - Conditions d'entree: - Une `purchase.line` existe deja. - Son champ `quantity_theorical` est modifie via `write`. - Un lot `virtual` est rattache a la ligne. - Resultat attendu: - Si `delta > 0`: - augmenter la quantite courante du lot `virtual` via `set_current_quantity` pour conserver l'historique `lot.qt.hist` - augmenter le `lot.qt` ouvert existant - si aucun `lot.qt` ouvert n'existe, en creer un nouveau avec le delta - Si `delta < 0`: - diminuer le `lot.qt` ouvert uniquement si la quantite ouverte disponible est suffisante - diminuer la quantite courante du lot `virtual` du meme delta - si aucun `lot.qt` ouvert n'existe ou si sa quantite est insuffisante, bloquer avec l'erreur `Please unlink or unmatch lot` - Definition du `lot.qt` ouvert: - `lot_p = virtual lot` - `lot_s = None` - `lot_shipment_in = None` - `lot_shipment_internal = None` - `lot_shipment_out = None` - Exceptions: - si aucun lot `virtual` n'est trouve sur la ligne, la regle ne fait rien - Priorite: - `bloquante` - Source: - `Decision metier documentee dans les commentaires de purchase_trade.purchase.Line.write` ### BR-PT-002 - Le lot physique est le pont metier entre purchase, sale et shipment - Intent: disposer d'un chemin unique et stable pour retrouver les informations logistiques et de facturation reliees a un contrat d'achat ou de vente. - Description: - Le lot physique (`lot_type = physic`) porte simultanement le lien vers: - la `purchase.line` via `lot.line` - la `sale.line` via `lot.sale_line` - le shipment via `lot.lot_shipment_in` / `lot.lot_shipment_internal` / `lot.lot_shipment_out` - Pour toute logique qui doit naviguer entre achat, vente, shipment et facture, il faut privilegier ce lot physique comme source de verite. - Resultat attendu: - depuis une facture d'achat: - remonter a la `purchase.line` - puis au lot physique de la ligne - puis au shipment et aux donnees logistiques associees - depuis une facture de vente: - remonter a la `sale.line` - puis au lot physique matchant qui porte aussi la `purchase.line` - puis au shipment et aux donnees logistiques associees - Cas d'usage typiques: - recuperer `bl_date`, `bl_number`, `controller`, `from_location`, `to_location` - retrouver une facture provisoire liee au lot - retrouver des fees rattaches au shipment - Priorite: - `structurante` ### BR-PT-003 - Le freight amount des templates facture vient du fee de shipment - Intent: afficher dans les documents facture la vraie valeur de fret maritime rattachee au shipment du lot physique. - Description: - Le `FREIGHT VALUE` d'une facture ne doit pas etre pris sur la facture elle-meme. - Il doit etre calcule a partir du `fee.fee` rattache au shipment (`shipment_in`) du lot physique relie a la facture. - Regle de navigation: - retrouver le lot physique pertinent depuis la facture - retrouver son shipment - chercher le `fee.fee` avec: - `shipment_in = shipment.id` - `product.name = 'Maritime freight'` - utiliser `fee.get_amount()` comme montant de fret - Portee: - s'applique aussi bien aux factures d'achat qu'aux factures de vente - cote vente, la remontee doit passer par le lot physique qui fait le lien entre `purchase.line` et `sale.line` - Priorite: - `importante` ### BR-PT-004 - La valuation doit couvrir les flux purchase et sale, y compris les sales non matchees - Intent: obtenir un PnL coherent cote achat et cote vente, meme lorsqu'une sale n'est pas encore matchee a une purchase. - Description: - Le flux historique de valuation part de `purchase.line` puis remonte vers les ventes via les lots/lots matchants. - Le systeme doit egalement savoir valoriser directement une `sale.line` non matchee ("sale-first"). - Une sale non matchee doit creer des lignes dans `valuation.valuation` et `valuation.valuation.line` afin d'apparaitre dans l'onglet PnL de la sale. - Resultat attendu: - pour une `sale.line` non matchee, generer au minimum les types: - `sale priced` - `sale fee` - `derivative` si la ligne porte des derives - si la sale est matchee via un lot physique, les lignes purchase portees par ce lot physique doivent aussi renseigner `sale` et `sale_line` - une sale matchee doit donc voir: - ses lignes `sale *` - les lignes purchase portees par le lot physique partage - avant creation du lot physique, si le matching existe seulement via `lot.qt` (`lot_p` purchase ouvert -> `lot_s` sale ouvert), les lignes PnL purchase-side doivent aussi renseigner `sale` et `sale_line` afin d'apparaitre dans l'onglet PnL de la sale matchee - un lot ouvert / virtuel avec quantite courante a zero ne doit pas generer de lignes de fees PnL residuelles - si plusieurs sales differentes sont matchees au meme lot ouvert, ne pas attacher arbitrairement une sale unique aux lignes purchase-side - Priorite: - `structurante` ### BR-PT-005 - Les references de valuation doivent decrire la nature du lot de la ligne - Intent: eviter les ambiguïtes dans les ecrans PnL entre lots `open` et lots `physic`. - Description: - La reference affichee dans la valuation doit decrire la ligne elle-meme, pas son vis-a-vis. - Les references autorisees pour les lignes de prix sont: - `Purchase/Open` - `Purchase/Physic` - `Sale/Open` - `Sale/Physic` - Resultat attendu: - un lot `virtual` cote purchase ne doit jamais sortir avec la reference `Purchase/Physic` - un lot `virtual` cote sale ne doit jamais sortir avec la reference `Sale/Physic` - un lot physique matche peut produire: - une ligne purchase en `Purchase/Physic` - une ligne sale en `Sale/Physic` - un open sale matche a un open purchase peut produire des quantites egales tout en gardant des references differentes (`Purchase/Open` vs `Sale/Open`) - Priorite: - `importante` ### BR-PT-006 - Une sale basis sans prix detaille doit quand meme apparaitre en valuation - Intent: ne pas perdre les lignes de PnL lorsque le detail de pricing n'est pas encore renseigne. - Description: - Une `sale.line` de type `basis` peut exister avec un lot `virtual`, sans `price_summary` et sans `lot_price_sale`. - Dans ce cas, la valuation doit quand meme creer une ligne `sale priced`. - Resultat attendu: - si `price_summary` est vide: - creer une ligne `sale priced` - avec `price = 0` - avec `amount = 0` - avec un `state` de type `unfixed` - si `lot_price_sale` est vide sur un lot sale, utiliser `sale_line.unit_price` comme fallback quand il existe - Priorite: - `importante` ### BR-PT-007 - Le MTM de valuation ne s'applique pas aux fees - Intent: distinguer les lignes de prix marquables au marche des lignes de frais qui ne doivent pas etre mark-to-market. - Description: - Le systeme peut renseigner `mtm_price`, `mtm` et `strategy` uniquement pour: - `pur. priced` - `sale priced` - `derivative` - Les fees (`pur. fee`, `sale fee`, `shipment fee`, `line fee`) ne doivent jamais porter de valorisation MTM. - Resultat attendu: - les lignes de fee doivent conserver: - `mtm_price = NULL` - `mtm = NULL` - `strategy = NULL` - `mtm_price` doit representer le prix brut de valorisation sans appliquer le ratio de composant - `mtm` reste le montant calcule selon la logique de strategie - Priorite: - `structurante` ### BR-PT-007-bis - Mark as finished ignore seulement le reliquat ouvert - Intent: conserver le PnL reel des lots executes tout en masquant le reliquat ouvert d'une ligne terminee. - Description: - Sur une `purchase.line` ou une `sale.line`, le champ `finished` (`Mark as finished`) ne signifie pas que la ligne ne doit plus etre valorisee. - Il signifie seulement que les lots ouverts / virtuels restants ne doivent plus alimenter la valuation. - Dans `Lots Management`, la meme regle s'applique aux lignes `lot.qt`: une ligne `lot.qt` doit etre masquee si son lot virtuel purchase (`lot_p`) ou son lot virtuel sale (`lot_s`) est rattache a une ligne marquee finie. - Cette exclusion vaut meme si la ligne `lot.qt` est deja matchee ou liee a un shipment. - Resultat attendu: - les lots physiques continuent de produire: - PnL prix - PnL fees - les derivatives continuent de produire du PnL meme si la ligne est marquee finie - les lots virtuels / ouverts d'une ligne finie sont ignores - les lots physiques restent visibles dans `Lots Management`, meme si leur ligne purchase ou sale est marquee finie - Priorite: - `structurante` ### BR-PT-008 - Le premium fait partie du prix contractuel en `priced` et en `basis` - Intent: garantir que le montant total valorise et facture reflete toujours le premium/discount saisi sur la ligne. - Description: - Le `premium` d'une `purchase.line` ou `sale.line` doit impacter le prix total quelle que soit la `price_type`. - Cette regle vaut pour: - les calculs de `amount` - la valuation / PnL - Resultat attendu: - le `unit_price` reste le prix de base, hors premium - en `priced`, le montant economique = `unit_price + premium` - en `basis`, le premium s'ajoute aussi au prix total economique - en valuation `basis`, le premium s'applique a chaque composant valorise (ex: meme premium repete sur chaque bloc ICE) - Exemple metier: - `8.30 USC/LB 500 TONS ON ICE MCH'26` - `8.30 USC/LB 500 TONS ON ICE MAY 26` - le premium `8.30 USC/LB` s'applique a chaque composant - Priorite: - `structurante` ### BR-PT-009 - En linked currency, le premium est exprime dans la devise/unite liee - Intent: respecter la facon dont les traders saisissent les prix sur certains produits (ex: coton en `USC/LB`). - Description: - Quand `enable_linked_currency` est coche, le `premium` est saisi dans la devise / unite liee, pas dans la devise / unite native de la ligne. - Le systeme doit convertir ce premium vers le repere de la ligne pour les calculs internes de montant et de valuation. - Resultat attendu: - `premium` est interprete dans le repere `linked_currency` / `linked_unit` - le `unit_price` ne doit pas absorber ce premium - les `amount` et valuations doivent refleter ce premium converti - si `linked currency` est cochee, `linked_price`, `linked_currency` et `linked_unit` sont obligatoires - Priorite: - `structurante` ### BR-PT-010 - En `basis + linked currency`, le linked price suit le basis brut - Intent: rendre lisible la decomposition entre prix basis de marche et premium. - Description: - Quand une ligne est en `basis` et `linked currency`, le bloc `linked_price` doit etre recalcule automatiquement. - Ce `linked_price` doit representer le prix basis brut, hors premium. - Le `unit_price` de la ligne doit rester ce prix brut converti. - Le premium converti n'est ajoute qu'au niveau du `amount`. - Resultat attendu: - modification du basis -> mise a jour automatique du `linked_price` - `linked_price` = base market / basis - `unit_price` = `linked_price` converti - `amount` = quantite * (`unit_price` + premium converti) - Priorite: - `importante` ### BR-PT-011 - Une sale line non matchee avec lot virtuel doit generer une valuation sale-first des la validation - Intent: ne pas attendre un matching purchase pour afficher le PnL d'une sale ouverte. - Description: - Lors de la validation d'une `sale.line`, le systeme peut creer un lot `virtual`. - Si aucun `lot.qt` ne relie ce lot a une `purchase.line`, il faut tout de meme generer la valuation cote sale. ### BR-PT-012 - Le wizard Create contracts peut creer un seul achat matche a plusieurs open sales - Intent: permettre la creation d'un contrat achat unique a partir de plusieurs `lot.qt` de vente selectionnes. - Description: - En mode `matched`, le wizard `Create contracts` peut recevoir plusieurs `lot.qt` selectionnes. - Il doit creer un seul contrat, avec une ligne par lot source selectionne. - Chaque ligne doit conserver son lot d'origine pour le matching. - Resultat attendu: - le wizard agrege les quantites de la selection - il refuse une quantite saisie differente du total selectionne - il conserve `created_by_code = True` sur les lignes creees pour ne pas declencher les creations automatiques parasites lors des validations - Priorite: - `importante` ### BR-PT-013 - Le texte par defaut de pricing_rule est configure globalement - Intent: centraliser un texte metier recurrent reutilise a la creation des lignes achat et vente. - Description: - Le module expose un singleton `purchase_trade.configuration` avec un champ texte `pricing_rule`. - Toute nouvelle `purchase.line` et `sale.line` doit prendre ce texte comme valeur par defaut de `pricing_rule`. - Resultat attendu: - la configuration est accessible depuis le menu `Prices` - la valeur sert de defaut a la creation des lignes - les lignes existantes ne sont pas modifiees retroactivement - Priorite: - `importante` ### BR-PT-014 - L'affectation d'un controller doit suivre l'ecart a l'objectif regional - Intent: repartir les controllers selon les cibles definies dans l'onglet `Execution` des `party.party`. - Description: - chaque ligne `party.execution` fixe une cible `% targeted` pour un controller sur une `country.region` - le `% achieved` est calcule a partir des `stock.shipment.in` deja affectes a un controller dans cette zone - la zone d'un shipment est determinee par `shipment.to_location.country` - une region parente couvre aussi ses sous-regions - Resultat attendu: - pour une ligne `party.execution`, `achieved_percent` = `shipments de la zone avec ce controller / shipments controles de la zone` - le denominateur ne compte que les `stock.shipment.in` qui ont deja un `controller`; les shipments encore non affectes ne biaisent donc pas la statistique affichee - lors d'un choix automatique de controller, la priorite va a la regle dont l'ecart `targeted - achieved` est le plus eleve - un controller a `80%` cible et `40%` reel doit donc passer avant un controller a `50%` cible et `45%` reel sur la meme zone - l'appartenance a la zone se lit depuis `shipment.to_location.country`, et une region parente couvre aussi ses sous-regions - Priorite: - `importante` ### BR-PT-014-bis - Les couts SLA controller peuvent cibler pays et/ou lieu - Intent: permettre de definir le cout d'un controller soit pour un pays, soit pour une location, soit pour un couple pays + location. - Description: - Dans l'onglet `Execution` de `party.party`, les lignes SLA (`party.execution.place`) peuvent porter: - `country` - `location` - ou les deux. - Lors de la creation automatique du fee controller sur un `stock.shipment.in`, le systeme continue de partir de `shipment.to_location`. - Le pays utilise pour le matching est `shipment.to_location.country`. - Priorite de matching: - couple `country + location` - puis `location` seule - puis `country` seul - Resultat attendu: - un cout defini uniquement sur un pays s'applique a toutes les destinations de ce pays. - un cout defini uniquement sur une location s'applique a cette destination, quel que soit le pays porte par la location. - un cout defini sur le couple pays + location est le plus specifique et prime les deux autres. - Priorite: - `importante` ### BR-PT-015 - Les weight reports distants par lot partent du weight report global attache au shipment - Intent: separer la creation du `weight.report` global et l'export detaille par lot vers le systeme distant. - Description: - l'automation cree le `weight.report` global et l'attache au `stock.shipment.in` - l'export FastAPI par lot ne part plus directement de l'automation - l'utilisateur ouvre le `weight.report` voulu depuis le shipment et lance l'action d'export depuis ce rapport - Resultat attendu: - le rapport choisi sert de base unique pour calculer les payloads par lot - seuls les lots physiques des `incoming_moves` du shipment sont exportes - l'action exige au minimum un `controller` et un `returned_id` sur le shipment - les cles renvoyees par le systeme distant et la date d'envoi sont conservees sur le `weight.report` local - Priorite: - `importante` ### BR-PT-016 - En pricing manuel, seules la quantite fixee du jour et le prix de marche sont saisis - Intent: simplifier la saisie utilisateur et garantir une coherence unique entre les colonnes de `pricing.pricing`. - Description: - Pour une ligne de `pricing.pricing` en mode manuel, l'utilisateur ne doit saisir que: - `quantity` - `settl_price` - Les autres colonnes de suivi sont derivees automatiquement sur tout le groupe metier (`line + component` ou `sale_line + component`) trie par `pricing_date`. - Resultat attendu: - `fixed_qt` = cumul des `quantity` - `fixed_qt_price` = moyenne ponderee cumulee des `settl_price` - `unfixed_qt` = quantite de base de la ligne - `fixed_qt` - `unfixed_qt_price` = dernier prix disponible de la courbe du composant quand le composant est lie a une courbe; sinon fallback sur `settl_price` de la ligne - `eod_price` = moyenne ponderee entre jambe fixee et non fixee - `last=True` reste unique par groupe et suit la plus grande `pricing_date` - Hors scope: - la generation automatique des lignes quand `pricing.component.auto = True` ne doit pas changer de comportement - Priorite: - `structurante` ### BR-PT-017 - Le workflow Validate des factures client doit aussi attribuer le numero - Intent: aligner le comportement des factures client et fournisseur au moment de `Validate`. - Description: - Lors du workflow `Validate` sur `account.invoice`, une facture client (`type = out`) doit maintenant: - creer son `account.move` - recevoir son `number` - La numerotation ne doit plus etre repoussee au `Post` cote client. - Resultat attendu: - a l'issue de `Validate`, une facture fournisseur ou client possede deja: - son `account.move` - son `number` - `Post` conserve son role de posting comptable sans reintroduire de difference de session/fresh login cote client - Priorite: - `importante` - Resultat attendu: - apres creation du lot virtuel, si aucun matching purchase n'existe: - appeler `Valuation.generate_from_sale_line(line)` - creer au moins la ligne `sale priced` fallback si la ligne porte un prix economique via le premium - Priorite: - `importante` ### BR-PT-018 - Les contrats distinguent le compte bancaire tiers du compte bancaire compagnie - Intent: eviter de confondre le compte bancaire du client/fournisseur avec le compte bancaire de la compagnie courante utilise pour encaisser ou payer. - Description: - Sur `sale.sale` et `purchase.purchase`, `bank_account` represente le compte bancaire propre a la `party` du contrat. - Sur `sale.sale` et `purchase.purchase`, `our_bank_account` represente le compte bancaire utilise par la compagnie courante pour encaisser ou payer. - `bank_account` est limite aux comptes bancaires de la party du contrat. - `our_bank_account` reste librement selectionnable parmi les comptes bancaires disponibles. - Resultat attendu: - si plusieurs comptes existent, le compte dont la devise correspond a la devise du contrat est propose en priorite - si aucun compte ne matche la devise, le premier compte disponible est propose - le champ `Our Bank Account` est pre-rempli depuis les comptes de la compagnie quand possible, mais sa recherche n'est pas limitee a ces comptes - Priorite: - `importante` ### BR-PT-019 - Le padding de facture provisoire vente augmente la quantite facturee sans modifier le lot physique - Intent: permettre de constituer une provision sur une facture provisoire vente tout en gardant la trace de l'ecart avec la quantite reelle du lot. - Description: - Le wizard `lot.invoice` expose un padding global uniquement pour les factures provisoires cote vente. - Ce padding global est reparti egalement entre les lots selectionnes. - La quantite de chaque ligne de facture provisoire vente est augmentee de la part de padding du lot. - Le padding ne modifie pas la quantite physique du lot. - Resultat attendu: - deux lots factures ensemble avec un padding global de `1000` recoivent chacun `500` de padding - la ligne facture affiche la quantite augmentee - la ligne facture expose `Inc. padding` - le lot conserve sa part de padding dans `sale_invoice_padding` - Validation comptable: - au `Validate`, le move principal de facture inclut deja le padding car il est integre a `account.invoice.line.quantity` - la provisoire cree un `additional_move` avec le couple de comptes configure `Default Sale Padding` / `Default Accrual Padding` - montant provisoire: `lot.sale_invoice_padding * account.invoice.line.unit_price` - la finale cree l'ecriture inverse pour exactement le montant padding comptabilise lors de la provisoire - le montant d'extourne finale doit etre relu depuis `lot.sale_invoice_line_prov`, afin de reprendre le prix, la devise, la date et le taux de la provisoire - le calcul de quantite finale doit retirer le padding de la quantite provisoire avant de calculer le delta - voir `modules/purchase_trade/docs/padding-invoice-accounting.md` - Priorite: - `importante` ### BR-PT-012 - Fallback valuation basis sans summary: utiliser le prix economique de la ligne - Intent: eviter qu'une valuation `basis` ouverte sorte a zero alors que la ligne a bien une valeur economique via le premium. - Description: - Une ligne `basis` peut ne pas avoir encore de `price_summary`. - Dans ce cas, la valuation fallback ne doit pas prendre `unit_price` seul si celui-ci est brut et hors premium. - Resultat attendu: - le fallback valuation `basis` doit utiliser: - `unit_price + premium converti` - cette regle vaut au minimum pour: - `sale.line` non matchee - `purchase.line` sans summary - Priorite: - `importante` ### BR-PT-013 - Create Contracts multi-lots doit conserver un matching par lot source - Intent: permettre la creation d'un seul contrat mirror a partir de plusieurs open quantities sans perdre le lien lot-a-lot. - Description: - Le wizard `Create contracts` peut etre lance avec plusieurs `lot.qt` selectionnes. - En creation `matched`, le systeme doit creer un seul contrat avec une ligne par lot source selectionne, et chaque ligne doit etre matchee avec son lot d'origine. - Resultat attendu: - la quantite totale du wizard = somme des open quantities selectionnees - le contrat cree porte plusieurs lignes si plusieurs lots source sont selectionnes - chaque ligne creee reutilise le `shipment_origin` et le lot source qui lui correspondent - `created_by_code` doit rester positionne sur les lignes creees par wizard pour eviter la recreation automatique de lots virtuels dans les `validate` de `purchase.line`, `sale.line` et `lot.lot` - Priorite: - `importante` ### BR-PT-014 - Delivery period: From doit rester inferieur ou egal a To - Intent: eviter les periodes de livraison incoherentes sur les lignes achat et vente. - Description: - Les champs `from_del` et `to_del` sont presents sur `purchase.line` et `sale.line`. - Si les deux dates sont renseignees, `from_del` ne doit jamais etre posterieur a `to_del`. - Resultat attendu: - la sauvegarde d'une `purchase.line` ou `sale.line` est bloquee si `from_del > to_del` - une date ouverte reste autorisee si seulement une des deux bornes est renseignee - Priorite: - `importante` ### BR-PT-015 - Pricing manuel: composant limite a la ligne courante - Intent: eviter qu'une ligne de pricing saisie manuellement utilise un composant rattache a une autre ligne de contrat. - Description: - Dans l'onglet `Pricing dates` d'une `purchase.line`, le champ `pricing.pricing.price_component` doit proposer uniquement les composants dont `pricing.component.line` est la ligne achat courante. - Dans l'onglet `Pricing dates` d'une `sale.line`, il doit proposer uniquement les composants dont `pricing.component.sale_line` est la ligne vente courante. - Une ligne de pricing sans composant reste possible pour le mode manuel sans component. - Resultat attendu: - le domaine UI filtre les composants sur la ligne courante - une validation serveur bloque aussi un composant appartenant a une autre ligne - Priorite: - `importante` ### BR-PT-016 - Les fees `% rate` utilisent le delta de financement - Intent: aligner le calcul des frais financiers `% rate` entre achat et vente. - Description: - Pour un `fee.fee` en mode `rate`, le calcul ne depend pas du `payment_term`, ni de la date du jour, ni de `fee_date`. - La periode de calcul est directement le champ `fin_int_delta` de la ligne `Estimated date` avec `trigger = bldate`. - Resultat attendu: - purchase et sale appliquent la meme formule: `amount = unit_price * quantity * (price / 100) * fin_int_delta / 360` - le montant affiche sur `fee.fee` doit rester non signe: un `fin_int_delta` negatif ne doit pas afficher un fee negatif - le PnL applique seul le signe metier: - `PAY` => montant negatif - `REC` => montant positif - si aucune Estimated Date `bldate` n'est renseignee, aucun montant `% rate` n'est calcule - Priorite: - `importante` ### BR-PT-020 - Le solde `lot.qt` ouvert suit les lots physiques existants - Intent: eviter qu'une hausse ou baisse de quantite contractuelle double le reliquat ouvert quand des lots physiques existent deja. - Description: - Lorsqu'une `purchase.line.quantity_theorical` ou `sale.line.quantity_theorical` est modifiee, le `lot.qt` libre ne doit pas etre ajuste uniquement par delta. - Le systeme doit recalculer le solde ouvert cible depuis la quantite contractuelle, les lots physiques deja crees et les quantites deja allouees dans des `lot.qt` matches ou shippes. - Resultat attendu: - quantite virtuelle cible = `quantity_theorical - somme(lots physiques convertis dans l'unite ligne)` - `lot.qt` libre non matche / non shippe = `quantite virtuelle cible - somme(lot.qt deja matches ou shippes)` - si le resultat devient negatif, bloquer avec `Please unlink or unmatch lot` - exemple: une ligne achat passee de `10000` a `20000` avec deja `10000` physiques doit afficher `10000` ouverts et `10000` physiques, pas `20000` ouverts plus `10000` physiques. - Impacts fees: - apres toute modification de `quantity_theorical`, les fees de la ligne sont resynchronises avec la regle BR-PT-021. - si le fee est encore uniquement porte par le lot virtuel, sa quantite suit donc la nouvelle quantite virtuelle. - si le fee possede deja des lots physiques, une hausse ou baisse uniquement ouverte n'impacte pas sa quantite. - Priorite: - `structurante` ### BR-PT-021 - Les fees lies aux lots privilegient les physiques - Intent: eviter qu'un fee cree sur un lot virtuel reste calcule sur la quantite contractuelle totale apres creation de lots physiques. - Detail implementation / PnL: - voir `modules/purchase_trade/docs/fees.md` - Description: - Le lien `fee.lots` avec le 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 du fee et le virtuel est ignore pour la quantite. - Resultat attendu: - si aucun physique n'existe, `fee.quantity` suit le lot virtuel rattache au fee. - si des physiques existent, `fee.quantity` suit uniquement la somme des lots physiques rattaches au fee. - pour `ppack`, la somme se fait sur `lot.lot_qt` des physiques. - pour les modes quantitatifs (`perqt`, `rate`, `pprice`, `pcost`), la somme se fait sur les quantites courantes converties des lots physiques. - supprimer le dernier physique fait retomber le fee sur son lot virtuel, puisque le lien virtuel est conserve. - le PnL fee applique la meme selection: full open tant qu'il n'y a que le virtuel, puis uniquement les lots physiques effectifs. - Points de synchronisation obligatoires: - creation d'un fee lie a une ligne ou a un shipment - ajout d'un lot physique dans `fee.lots` - modification de `purchase.line.quantity_theorical` ou `sale.line.quantity_theorical` - weighing / modification de quantite d'un lot physique - suppression d'un lot physique ou suppression d'un lien `fee.lots` - Regle de conception: - ne pas supprimer le lien `fee.lots` vers le lot virtuel lors de la creation de physiques. - le virtuel reste le fallback permettant de revenir au cas ouvert si tous les physiques sont supprimes. - toute logique metier, comptable ou PnL doit utiliser les lots effectifs du fee: physiques s'il y en a, sinon virtuels. - Priorite: - `structurante` ### BR-PT-022 - Remove physical lot restaure le lot.qt ouvert avec son contexte - Intent: permettre d'annuler un lot physique cree par erreur sans perdre le contexte metier de shipment ou de matching deja porte par ce lot. - Description: - L'action `Remove physical lot` peut supprimer un lot physique meme s'il est shippe, matche, ou les deux. - Dans ces cas, l'utilisateur doit recevoir un warning confirmable avant la suppression. - La suppression n'est autorisee que si le `stock.move` lie au lot physique est encore en etat `draft`. - Si le `stock.move` n'est pas en `draft`, l'action est bloquee car le flux peut deja avoir des impacts stock/comptables. - Resultat attendu: - le `lot.move` et le `stock.move` correspondant sont supprimes avec le lot physique. - la quantite courante convertie du lot physique est restauree dans `lot.qt`. - si le lot etait shippe, la ligne `lot.qt` restauree conserve le shipment (`lot_shipment_in`, `lot_shipment_internal` ou `lot_shipment_out`). - si le lot etait matche, la ligne `lot.qt` restauree conserve le lot sale virtuel (`lot_s`). - si une ligne `lot.qt` existe deja avec le meme lot purchase virtuel, le meme shipment et le meme `lot_s`, la quantite est agregee sur cette ligne plutot que de creer un doublon. - si aucune ligne compatible n'existe, une nouvelle ligne `lot.qt` est creee. - Priorite: - `importante` ### BR-PT-023 - Lots Management separe matching, side et shipping status - Intent: rendre le rapport `lot.report` exploitable sans melanger le statut de matching, le sens achat/vente et l'avancement logistique. - Description: - Le filtre `Matching status` ne porte que le matching commercial: `All`, `Matched`, `Not Matched`. - Le filtre `Side` permet de lire: `All`, `Purchase`, `Sale`. - Le filtre `Shipping status` porte uniquement le lien `shipment_in` du `lot.lot` ou du `lot.qt`. - Les dates de contexte `As of` et `To` filtrent les contrats via `purchase.purchase_date` et `sale.sale_date`. - Si le filtre `Side` vaut `Purchase`, les bornes de dates s'appliquent au `purchase_date`. - Si le filtre `Side` vaut `Sale`, les bornes de dates s'appliquent au `sale_date`. - Si le filtre `Side` vaut `All`, une ligne est conservee si son `purchase_date` ou son `sale_date` entre dans les bornes. - Le filtre `Dimension` du rapport cible une valeur de dimension analytique rattachee au contrat achat ou vente via `analytic.dimension.assignment`. - Le filtre `Strategy` cible les strategies MTM rattachees aux lignes achat ou vente (`purchase.strategy` / `sale.strategy`). - Resultat attendu: - `Unshipped`: aucun `shipment_in` lie au `lot.lot` ou au `lot.qt`. - `Scheduled`: un `shipment_in` est lie et son etat est `draft`. - `Shipped`: un `shipment_in` est lie et son etat est `started`. - `Received`: un `shipment_in` est lie et son etat est `received` ou `done`. - Le rapport affiche une colonne `Shipping status`. - Le rapport affiche `Purchase Delivery Period` depuis la ligne achat et `Sale Delivery Period` depuis la ligne vente; l'ancien champ mixte `Del Period` ne doit plus prendre la periode sale comme fallback achat. - Shipment type: - Sur `stock.shipment.in` et dans `lot.report`, `Shipment Type` vaut `Dropship` si `from_location.type = supplier` et `to_location.type = customer`. - Dans tous les autres cas, `Shipment Type` vaut `Inbound`. - Priorite: - `importante` ### BR-PT-024 - Create Contracts propage les lieux stock selon le flux miroir - Intent: eviter une ressaisie des lieux logistiques quand un contrat miroir est cree depuis une quantite ouverte. - Description: - Les champs concernes sont les `stock.location` `from_location` et `to_location` des contrats achat et vente. - Si le contrat source est en flux direct fournisseur -> client (`from_location.type = supplier` et `to_location.type = customer`), le contrat cree reprend le meme couple `from_location` / `to_location`. - Ce flux correspond au mode `Dropship` affiche sur les shipments. - Si un contrat vente est cree depuis un achat dont `to_location.type = storage`, le `from_location` de la vente est pre-rempli avec ce `to_location` achat. - Si un contrat achat est cree depuis une vente dont `from_location.type = storage`, le `to_location` de l'achat est pre-rempli avec ce `from_location` vente. - Resultat attendu: - achat supplier -> customer vers vente: `sale.from_location = purchase.from_location` et `sale.to_location = purchase.to_location`. - vente supplier -> customer vers achat: `purchase.from_location = sale.from_location` et `purchase.to_location = sale.to_location`. - achat vers stock puis vente: `sale.from_location = purchase.to_location`. - vente depuis stock puis achat: `purchase.to_location = sale.from_location`. - Priorite: - `importante` ## 4) Exemples concrets ### Exemple E1 - Augmentation simple - Donnees: - `ancienne quantity_theorical = 100` - `nouvelle quantity_theorical = 120` - `lot.qt ouvert = 40` - Attendu: - lot `virtual` augmente de `20` - `lot.qt ouvert` passe de `40` a `60` ### Exemple E2 - Augmentation sans lot.qt ouvert - Donnees: - `ancienne quantity_theorical = 100` - `nouvelle quantity_theorical = 110` - aucun `lot.qt` ouvert - Attendu: - lot `virtual` augmente de `10` - creation d'un `lot.qt` ouvert a `10` ### Exemple E3 - Diminution possible - Donnees: - `ancienne quantity_theorical = 100` - `nouvelle quantity_theorical = 90` - `lot.qt ouvert = 25` - Attendu: - lot `virtual` diminue de `10` - `lot.qt ouvert` passe de `25` a `15` ### Exemple E4 - Diminution impossible - Donnees: - `ancienne quantity_theorical = 100` - `nouvelle quantity_theorical = 80` - `lot.qt ouvert = 5` - Attendu: - blocage avec `Please unlink or unmatch lot` ## 5) Impact code attendu - Fichiers Python concernes: - `modules/purchase_trade/purchase.py` - `modules/purchase_trade/lot.py` - `modules/purchase_trade/valuation.py` - `modules/purchase_trade/sale.py` ## 6) Strategie de tests Pour cette regle, couvrir au minimum: - augmentation avec `lot.qt` ouvert existant - augmentation sans `lot.qt` ouvert - diminution possible - diminution impossible avec erreur - valuation purchase/sale sur lot physique matche - valuation sale-first sur sale non matchee avec lot virtual - valuation sale `basis` sans `price_summary` - absence de MTM sur les fees - premium en `priced` - premium en `basis` - premium en `linked currency` - synchro `basis` -> `linked_price` -> `unit_price` ## 7) Notes de fin de session ### Session 2026-04-30 - PnL fees ouverts et `% rate` - Les fees PnL ne doivent pas etre generes pour un lot ouvert / virtuel dont la quantite courante est a zero. - Cette regle evite les lignes PnL residuelles avec `quantity = 0` mais `amount != 0`, notamment pour les fees `rate`, `ppack` et `lumpsum`. - La logique est volontairement proche de `Mark as finished`: quand le reliquat ouvert/virtuel n'est plus valorisable, ses fees ne le sont pas non plus. - Les lots physiques restent hors de ce filtre. - Pour les fees `% rate`, `fin_int_delta` est la periode absolue de calcul. - Le calcul `% rate` ne depend plus de la date du jour, de `fee_date`, ni de `BL date + delta` comme date de fin. - Formule commune purchase/sale: `amount = unit_price * quantity * (price / 100) * fin_int_delta / 360`. - La ligne `Estimated date` avec `trigger = bldate` sert a porter `fin_int_delta`; la date estimee n'entre pas dans le calcul du montant. - Le montant fee affiche reste toujours non signe, meme si `fin_int_delta` est negatif; le signe est applique uniquement en PnL via `PAY` / `REC`. ### Session 2026-05-09 - Lots Management et Apply matching - Le rapport `Lots Management` separe maintenant les filtres: `Matching status`, `Shipping status`, `Side`, `Dimension` et `Strategy`. - Les bornes `As of` / `To` filtrent sur `purchase.purchase_date` et `sale.sale_date` uniquement quand elles sont renseignees; elles ne doivent pas filtrer par defaut a l'ouverture du report. - Le filtre `Dimension` cible une valeur dynamique de dimension analytique via `analytic.dimension.assignment.value`, pour les achats comme pour les ventes. - Le filtre `Strategy` cible les strategies MTM rattachees aux lignes achat ou vente (`purchase.strategy` / `sale.strategy`). - `Apply matching` doit proposer les reliquats ouverts meme si le meme lot purchase ou sale possede deja une autre ligne `lot.qt` matchee. - Une ligne est exclue ou bloquee dans `Apply matching` seulement si la ligne `lot.qt` selectionnee est elle-meme deja matchee: - cote purchase: `lot_p` renseigne et `lot_s` vide - cote sale: `lot_s` renseigne et `lot_p` vide - Exemple: un achat de `1000` avec `800` deja matches et `200` encore ouverts doit laisser le reliquat `200` selectionnable dans `Apply matching`. - Dans `Link to transport`, le type de shipment reste impose a `Shipment In`; l'utilisateur ne doit pas pouvoir basculer vers Out/Internal depuis cette action. - `Mark as finished` dans `Lots Management` masque uniquement les reliquats ouverts / virtuels portes par `lot.qt`. - Le filtre doit regarder les deux cotes presents sur la ligne `lot.qt`: `lot_p.line.finished` cote purchase et `lot_s.sale_line.finished` cote sale, meme quand le filtre `Side` vaut `All`. - Les lots physiques ne doivent pas etre masques par ce flag; ils restent visibles pour conserver l'historique execute. ### Session 2026-05-17 - Tolerances, jauges et Go to matching - `Apply matching` est remplace cote utilisateur par `Go to matching`; l'action legacy est conservee mais masquee. - `Go to matching` precharge les lignes `lot.qt` ouvertes selectionnees depuis `Lots Management`. - Le matching ouvert peut depasser le solde ouvert strict si la quantite projetee reste dans la tolerance de la ligne (`Qt max`). - Les lignes de matching affichent `Qt min`, `Qt max` et une jauge `Tolerance used`. - Dans `Go to matching`, la jauge est dynamique: elle projette `deja matche + Qt to match` contre `quantity_theorical`, avec bornes `-tol_min` / `tol_max`. - Les jauges de `purchase.line` et `sale.line` utilisent: - la somme des lots physiques s'il en existe sur la ligne; - sinon la somme des `lot.qt` rattaches a la ligne. - Les jauges header `purchase.purchase` / `sale.sale` representent la moyenne ponderee des jauges de lignes, au prorata de `quantity_theorical`. - Cote `sale`, les quantites `lot.qt` sont lues en valeur absolue pour les jauges de tolerance. - Le widget SAO `tolerance_gauge` est autorise dans les schemas de vues `form` et `tree` avec `min`, `max`, `min_field`, `max_field`, `center` et `digits`. - En formulaire de ligne (`purchase.line` / `sale.line`), ne pas encapsuler `tolerance_gauge` et `targeted_qt` dans un sous-groupe `colspan="4"`: cela decale visuellement le bloc vers la droite dans SAO. Les placer directement dans la grille principale et forcer `xalign="0"` sur la jauge et sur `targeted_qt` pour garder un alignement a gauche stable.