Files
tradon/modules/purchase_trade/docs/business-rules.md
2026-06-11 10:49:05 +02:00

927 lines
40 KiB
Markdown

# 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.