Files
tradon/modules/purchase_trade/docs/business-rules.md

17 KiB

Business Rules - Purchase Trade

Statut: draft Version: v0.4 Derniere mise a jour: 2026-04-02 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.

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
    • 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
  • 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-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