373 lines
14 KiB
Markdown
373 lines
14 KiB
Markdown
# Business Rules - Purchase Trade
|
|
|
|
Statut: `draft`
|
|
Version: `v0.3`
|
|
Derniere mise a jour: `2026-04-01`
|
|
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
|
|
- 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-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.
|
|
- 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-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`
|
|
|
|
## 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`
|