Files
tradon/modules/purchase_trade/docs/business-rules.md
2026-05-20 09:52:27 +02:00

40 KiB

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