Bug pnl fee
This commit is contained in:
@@ -19,7 +19,7 @@ Statut: `migration partielle`
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Fees, freight, lots effectifs, <code>% rate</code>: <a href="fees.md">fees.md</a>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Valuation, PnL, MTM, derivatives: <a href="valuation-pnl-mtm.md">valuation-pnl-mtm.md</a>
|
||||
<li style="margin:0.38rem 0;">Valuation, PnL, MTM, derivatives: <a href="valuation-pnl-mtm.md">FR</a> / <a href="valuation-pnl-mtm.en.md">EN</a>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Factures provisoires/finales, padding: <a href="invoicing.md">invoicing.md</a>
|
||||
</li>
|
||||
@@ -78,6 +78,8 @@ Statut: `migration partielle`
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-VAL-003</code>: MTM hors fees.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-VAL-004</code>: snapshot courant PnL et identite economique.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-INV-001</code>: padding facture provisoire vente.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>BR-PT-ACC-001</code>: Validate facture client attribue le numero.
|
||||
|
||||
132
modules/purchase_trade/docs/business/valuation-pnl-mtm.en.md
Normal file
132
modules/purchase_trade/docs/business/valuation-pnl-mtm.en.md
Normal file
@@ -0,0 +1,132 @@
|
||||
<!-- Generated from docs_source/business by docs/tools/render_business_docs.py. -->
|
||||
|
||||
# Valuation, PnL, MTM
|
||||
|
||||
Status: `partial migration`
|
||||
|
||||
Language: `en`<br>
|
||||
Mirror page: [valuation-pnl-mtm.md](valuation-pnl-mtm.md)
|
||||
|
||||
## BR-PT-VAL-001 - Valuation covers purchase, sale, and sale-first flows
|
||||
|
||||
Source: `BR-PT-004`, `BR-PT-006`, `BR-PT-011`
|
||||
|
||||
### Consultant Rule
|
||||
|
||||
PnL must exist for purchases and for sales, even when a sale is not yet matched
|
||||
to a purchase.
|
||||
|
||||
### Developer Notes
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">An unmatched <code>sale.line</code> must generate at least <code>sale priced</code>, <code>sale fee</code>, and <code>derivative</code> when applicable.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">A basis sale with no price detail must still produce a zero line or the economic fallback price according to the applicable rule.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Do not arbitrarily attach a single sale when several sales are matched to the same open balance.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## BR-PT-VAL-002 - Valuation references
|
||||
|
||||
Source: `BR-PT-005`
|
||||
|
||||
### Consultant Rule
|
||||
|
||||
The PnL reference must describe the nature of the valued line: purchase or sale,
|
||||
open or physical.
|
||||
|
||||
### Developer Notes
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Allowed references: <code>Purchase/Open</code>, <code>Purchase/Physic</code>, <code>Sale/Open</code>, <code>Sale/Physic</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">A virtual lot must not be output with a physical reference.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## BR-PT-VAL-003 - MTM excludes fees
|
||||
|
||||
Source: `BR-PT-007`
|
||||
|
||||
### Consultant Rule
|
||||
|
||||
Mark-to-market applies to prices and derivatives, not to fees.
|
||||
|
||||
### Developer Notes
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">MTM is allowed for <code>pur. priced</code>, <code>sale priced</code>, <code>derivative</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Fees are outside MTM: <code>pur. fee</code>, <code>sale fee</code>, <code>shipment fee</code>, <code>line fee</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">For fees: <code>mtm_price</code>, <code>mtm</code>, <code>strategy</code> must stay empty.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## BR-PT-VAL-004 - Current snapshot and economic identity
|
||||
|
||||
Source: session `2026-05-14`
|
||||
|
||||
### Consultant Rule
|
||||
|
||||
`valuation_valuation_line` represents the latest known PnL image.
|
||||
It is not a history table.
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">A new generation replaces the previous generation for the same economic reality.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">An unmatched sale owns its own PnL.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">A matched sale is owned by the linked purchase line.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">The PnL of a matched sale must not be generated twice: once from the sale side and once from the purchase side.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### Developer Notes
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Before creating rows in <code>valuation.valuation.line</code>, delete the current snapshot with the same economic identity.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Economic identity:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;"><code>sale_line</code> when present, otherwise <code>line</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>lot</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>type</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>reference</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>counterparty</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>product</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>state</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>strategy</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Do not include in the identity:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;"><code>date</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>price</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>quantity</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>amount</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>mtm_price</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>mtm</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">These fields are recalculated results and must be replaced by the latest generation.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>generate_from_sale_line()</code> does not create a sale snapshot when the <code>sale.line</code> is already matched to a <code>purchase.line</code>; it redirects to the owner purchase line generation.
|
||||
</li>
|
||||
</ul>
|
||||
@@ -4,39 +4,42 @@
|
||||
|
||||
Statut: `migration partielle`
|
||||
|
||||
Langue: `fr`<br>
|
||||
Page miroir: [valuation-pnl-mtm.en.md](valuation-pnl-mtm.en.md)
|
||||
|
||||
## BR-PT-VAL-001 - La valuation couvre achat, vente et sale-first
|
||||
|
||||
Source: `BR-PT-004`, `BR-PT-006`, `BR-PT-011`
|
||||
|
||||
### Regle consultant
|
||||
### Règle consultant
|
||||
|
||||
Le PnL doit exister pour les achats et pour les ventes, meme quand une vente
|
||||
n'est pas encore matchee a un achat.
|
||||
Le PnL doit exister pour les achats et pour les ventes, même quand une vente
|
||||
n'est pas encore matchée à un achat.
|
||||
|
||||
### Notes developpeur
|
||||
### Notes développeur
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Une <code>sale.line</code> non matchee doit generer au minimum <code>sale priced</code>, <code>sale fee</code> et <code>derivative</code> si applicable.
|
||||
<li style="margin:0.38rem 0;">Une <code>sale.line</code> non matchée doit générer au minimum <code>sale priced</code>, <code>sale fee</code> et <code>derivative</code> si applicable.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Une sale basis sans detail de prix doit quand meme produire une ligne a zero ou au prix economique fallback selon la regle applicable.
|
||||
<li style="margin:0.38rem 0;">Une sale basis sans détail de prix doit quand même produire une ligne à zéro ou au prix économique fallback selon la règle applicable.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Ne pas attacher arbitrairement une sale unique si plusieurs sales sont matchees au meme ouvert.
|
||||
<li style="margin:0.38rem 0;">Ne pas attacher arbitrairement une sale unique si plusieurs sales sont matchées au même ouvert.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## BR-PT-VAL-002 - References de valuation
|
||||
## BR-PT-VAL-002 - Références de valuation
|
||||
|
||||
Source: `BR-PT-005`
|
||||
|
||||
### Regle consultant
|
||||
### Règle consultant
|
||||
|
||||
La reference de PnL doit decrire la nature de la ligne valorisee: achat ou
|
||||
La référence de PnL doit décrire la nature de la ligne valorisée: achat ou
|
||||
vente, ouverte ou physique.
|
||||
|
||||
### Notes developpeur
|
||||
### Notes développeur
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">References autorisees: <code>Purchase/Open</code>, <code>Purchase/Physic</code>, <code>Sale/Open</code>, <code>Sale/Physic</code>.
|
||||
<li style="margin:0.38rem 0;">Références autorisées: <code>Purchase/Open</code>, <code>Purchase/Physic</code>, <code>Sale/Open</code>, <code>Sale/Physic</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Un lot virtuel ne doit pas sortir avec une reference physique.
|
||||
</li>
|
||||
@@ -46,17 +49,84 @@ vente, ouverte ou physique.
|
||||
|
||||
Source: `BR-PT-007`
|
||||
|
||||
### Regle consultant
|
||||
### Règle consultant
|
||||
|
||||
Le mark-to-market s'applique aux prix et aux derives, pas aux frais.
|
||||
Le mark-to-market s'applique aux prix et aux dérivés, pas aux frais.
|
||||
|
||||
### Notes developpeur
|
||||
### Notes développeur
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">MTM autorise pour <code>pur. priced</code>, <code>sale priced</code>, <code>derivative</code>.
|
||||
<li style="margin:0.38rem 0;">MTM autorisé pour <code>pur. priced</code>, <code>sale priced</code>, <code>derivative</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Fees hors MTM: <code>pur. fee</code>, <code>sale fee</code>, <code>shipment fee</code>, <code>line fee</code>.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Pour les fees: <code>mtm_price</code>, <code>mtm</code>, <code>strategy</code> doivent rester vides.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## BR-PT-VAL-004 - Snapshot courant et identité économique
|
||||
|
||||
Source: session `2026-05-14`
|
||||
|
||||
### Règle consultant
|
||||
|
||||
`valuation_valuation_line` représente la dernière image connue du PnL.
|
||||
Elle n'est pas un historique.
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Une nouvelle génération remplace la génération précédente pour la même réalité économique.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Une vente non matchée porte son propre PnL.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Une vente matchée est portée par la ligne d'achat liée.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Le PnL d'une vente matchée ne doit pas être généré deux fois: une fois côté vente et une fois côté achat.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### Notes développeur
|
||||
|
||||
<ul style="margin:0.65rem 0 1rem 1.1rem; padding-left:1rem; list-style-type:disc;">
|
||||
<li style="margin:0.38rem 0;">Avant création dans <code>valuation.valuation.line</code>, supprimer le snapshot courant ayant la même identité économique.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Identité économique:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;"><code>sale_line</code> si elle existe, sinon <code>line</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>lot</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>type</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>reference</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>counterparty</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>product</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>state</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>strategy</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Ne pas inclure dans l'identité:
|
||||
<ul style="margin:0.65rem 0 1rem 1.35rem; padding-left:1rem; list-style-type:circle;">
|
||||
<li style="margin:0.38rem 0;"><code>date</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>price</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>quantity</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>amount</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>mtm_price</code>;
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>mtm</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;">Ces champs sont des résultats recalculés et doivent être remplacés par la dernière génération.
|
||||
</li>
|
||||
<li style="margin:0.38rem 0;"><code>generate_from_sale_line()</code> ne crée pas de snapshot vente si la <code>sale.line</code> est déjà matchée à une <code>purchase.line</code>; il redirige vers la génération de la ligne d'achat propriétaire.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,7 +10,7 @@ Statut: `migration partielle`
|
||||
- Shipments, controllers, SLA, weight reports: [shipments-execution.md](shipments-execution.md)
|
||||
- Pricing manuel, basis, premium, linked currency: [pricing.md](pricing.md)
|
||||
- Fees, freight, lots effectifs, `% rate`: [fees.md](fees.md)
|
||||
- Valuation, PnL, MTM, derivatives: [valuation-pnl-mtm.md](valuation-pnl-mtm.md)
|
||||
- Valuation, PnL, MTM, derivatives: [FR](valuation-pnl-mtm.md) / [EN](valuation-pnl-mtm.en.md)
|
||||
- Factures provisoires/finales, padding: [invoicing.md](invoicing.md)
|
||||
- Impacts `account.move`, validate/post: [accounting-bridge.md](accounting-bridge.md)
|
||||
- Comptes bancaires, payment terms, payment orders: [payments-banking.md](payments-banking.md)
|
||||
@@ -41,6 +41,7 @@ Statut: `migration partielle`
|
||||
- `BR-PT-VAL-001`: valuation achat/vente et sale-first.
|
||||
- `BR-PT-VAL-002`: references de valuation.
|
||||
- `BR-PT-VAL-003`: MTM hors fees.
|
||||
- `BR-PT-VAL-004`: snapshot courant PnL et identite economique.
|
||||
- `BR-PT-INV-001`: padding facture provisoire vente.
|
||||
- `BR-PT-ACC-001`: Validate facture client attribue le numero.
|
||||
- `BR-PT-PAY-001`: comptes bancaires tiers vs compagnie.
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# Valuation, PnL, MTM
|
||||
|
||||
Status: `partial migration`
|
||||
|
||||
Language: `en`<br>
|
||||
Mirror page: [valuation-pnl-mtm.md](valuation-pnl-mtm.md)
|
||||
|
||||
## BR-PT-VAL-001 - Valuation covers purchase, sale, and sale-first flows
|
||||
|
||||
Source: `BR-PT-004`, `BR-PT-006`, `BR-PT-011`
|
||||
|
||||
### Consultant Rule
|
||||
|
||||
PnL must exist for purchases and for sales, even when a sale is not yet matched
|
||||
to a purchase.
|
||||
|
||||
### Developer Notes
|
||||
|
||||
- An unmatched `sale.line` must generate at least `sale priced`, `sale fee`,
|
||||
and `derivative` when applicable.
|
||||
- A basis sale with no price detail must still produce a zero line or the
|
||||
economic fallback price according to the applicable rule.
|
||||
- Do not arbitrarily attach a single sale when several sales are matched to the
|
||||
same open balance.
|
||||
|
||||
## BR-PT-VAL-002 - Valuation references
|
||||
|
||||
Source: `BR-PT-005`
|
||||
|
||||
### Consultant Rule
|
||||
|
||||
The PnL reference must describe the nature of the valued line: purchase or sale,
|
||||
open or physical.
|
||||
|
||||
### Developer Notes
|
||||
|
||||
- Allowed references: `Purchase/Open`, `Purchase/Physic`, `Sale/Open`,
|
||||
`Sale/Physic`.
|
||||
- A virtual lot must not be output with a physical reference.
|
||||
|
||||
## BR-PT-VAL-003 - MTM excludes fees
|
||||
|
||||
Source: `BR-PT-007`
|
||||
|
||||
### Consultant Rule
|
||||
|
||||
Mark-to-market applies to prices and derivatives, not to fees.
|
||||
|
||||
### Developer Notes
|
||||
|
||||
- MTM is allowed for `pur. priced`, `sale priced`, `derivative`.
|
||||
- Fees are outside MTM: `pur. fee`, `sale fee`, `shipment fee`, `line fee`.
|
||||
- For fees: `mtm_price`, `mtm`, `strategy` must stay empty.
|
||||
|
||||
## BR-PT-VAL-004 - Current snapshot and economic identity
|
||||
|
||||
Source: session `2026-05-14`
|
||||
|
||||
### Consultant Rule
|
||||
|
||||
`valuation_valuation_line` represents the latest known PnL image.
|
||||
It is not a history table.
|
||||
|
||||
- A new generation replaces the previous generation for the same economic
|
||||
reality.
|
||||
- An unmatched sale owns its own PnL.
|
||||
- A matched sale is owned by the linked purchase line.
|
||||
- The PnL of a matched sale must not be generated twice: once from the sale
|
||||
side and once from the purchase side.
|
||||
|
||||
### Developer Notes
|
||||
|
||||
- Before creating rows in `valuation.valuation.line`, delete the current
|
||||
snapshot with the same economic identity.
|
||||
- Economic identity:
|
||||
- `sale_line` when present, otherwise `line`;
|
||||
- `lot`;
|
||||
- `type`;
|
||||
- `reference`;
|
||||
- `counterparty`;
|
||||
- `product`;
|
||||
- `state`;
|
||||
- `strategy`.
|
||||
- Do not include in the identity:
|
||||
- `date`;
|
||||
- `price`;
|
||||
- `quantity`;
|
||||
- `amount`;
|
||||
- `mtm_price`;
|
||||
- `mtm`.
|
||||
- These fields are recalculated results and must be replaced by the latest
|
||||
generation.
|
||||
- `generate_from_sale_line()` does not create a sale snapshot when the
|
||||
`sale.line` is already matched to a `purchase.line`; it redirects to the
|
||||
owner purchase line generation.
|
||||
@@ -2,36 +2,39 @@
|
||||
|
||||
Statut: `migration partielle`
|
||||
|
||||
Langue: `fr`<br>
|
||||
Page miroir: [valuation-pnl-mtm.en.md](valuation-pnl-mtm.en.md)
|
||||
|
||||
## BR-PT-VAL-001 - La valuation couvre achat, vente et sale-first
|
||||
|
||||
Source: `BR-PT-004`, `BR-PT-006`, `BR-PT-011`
|
||||
|
||||
### Regle consultant
|
||||
### Règle consultant
|
||||
|
||||
Le PnL doit exister pour les achats et pour les ventes, meme quand une vente
|
||||
n'est pas encore matchee a un achat.
|
||||
Le PnL doit exister pour les achats et pour les ventes, même quand une vente
|
||||
n'est pas encore matchée à un achat.
|
||||
|
||||
### Notes developpeur
|
||||
### Notes développeur
|
||||
|
||||
- Une `sale.line` non matchee doit generer au minimum `sale priced`, `sale fee`
|
||||
- Une `sale.line` non matchée doit générer au minimum `sale priced`, `sale fee`
|
||||
et `derivative` si applicable.
|
||||
- Une sale basis sans detail de prix doit quand meme produire une ligne a zero
|
||||
ou au prix economique fallback selon la regle applicable.
|
||||
- Une sale basis sans détail de prix doit quand même produire une ligne à zéro
|
||||
ou au prix économique fallback selon la règle applicable.
|
||||
- Ne pas attacher arbitrairement une sale unique si plusieurs sales sont
|
||||
matchees au meme ouvert.
|
||||
matchées au même ouvert.
|
||||
|
||||
## BR-PT-VAL-002 - References de valuation
|
||||
## BR-PT-VAL-002 - Références de valuation
|
||||
|
||||
Source: `BR-PT-005`
|
||||
|
||||
### Regle consultant
|
||||
### Règle consultant
|
||||
|
||||
La reference de PnL doit decrire la nature de la ligne valorisee: achat ou
|
||||
La référence de PnL doit décrire la nature de la ligne valorisée: achat ou
|
||||
vente, ouverte ou physique.
|
||||
|
||||
### Notes developpeur
|
||||
### Notes développeur
|
||||
|
||||
- References autorisees: `Purchase/Open`, `Purchase/Physic`, `Sale/Open`,
|
||||
- Références autorisées: `Purchase/Open`, `Purchase/Physic`, `Sale/Open`,
|
||||
`Sale/Physic`.
|
||||
- Un lot virtuel ne doit pas sortir avec une reference physique.
|
||||
|
||||
@@ -39,13 +42,54 @@ vente, ouverte ou physique.
|
||||
|
||||
Source: `BR-PT-007`
|
||||
|
||||
### Regle consultant
|
||||
### Règle consultant
|
||||
|
||||
Le mark-to-market s'applique aux prix et aux derives, pas aux frais.
|
||||
Le mark-to-market s'applique aux prix et aux dérivés, pas aux frais.
|
||||
|
||||
### Notes developpeur
|
||||
### Notes développeur
|
||||
|
||||
- MTM autorise pour `pur. priced`, `sale priced`, `derivative`.
|
||||
- MTM autorisé pour `pur. priced`, `sale priced`, `derivative`.
|
||||
- Fees hors MTM: `pur. fee`, `sale fee`, `shipment fee`, `line fee`.
|
||||
- Pour les fees: `mtm_price`, `mtm`, `strategy` doivent rester vides.
|
||||
|
||||
## BR-PT-VAL-004 - Snapshot courant et identité économique
|
||||
|
||||
Source: session `2026-05-14`
|
||||
|
||||
### Règle consultant
|
||||
|
||||
`valuation_valuation_line` représente la dernière image connue du PnL.
|
||||
Elle n'est pas un historique.
|
||||
|
||||
- Une nouvelle génération remplace la génération précédente pour la même
|
||||
réalité économique.
|
||||
- Une vente non matchée porte son propre PnL.
|
||||
- Une vente matchée est portée par la ligne d'achat liée.
|
||||
- Le PnL d'une vente matchée ne doit pas être généré deux fois: une fois côté
|
||||
vente et une fois côté achat.
|
||||
|
||||
### Notes développeur
|
||||
|
||||
- Avant création dans `valuation.valuation.line`, supprimer le snapshot courant
|
||||
ayant la même identité économique.
|
||||
- Identité économique:
|
||||
- `sale_line` si elle existe, sinon `line`;
|
||||
- `lot`;
|
||||
- `type`;
|
||||
- `reference`;
|
||||
- `counterparty`;
|
||||
- `product`;
|
||||
- `state`;
|
||||
- `strategy`.
|
||||
- Ne pas inclure dans l'identité:
|
||||
- `date`;
|
||||
- `price`;
|
||||
- `quantity`;
|
||||
- `amount`;
|
||||
- `mtm_price`;
|
||||
- `mtm`.
|
||||
- Ces champs sont des résultats recalculés et doivent être remplacés par la
|
||||
dernière génération.
|
||||
- `generate_from_sale_line()` ne crée pas de snapshot vente si la `sale.line`
|
||||
est déjà matchée à une `purchase.line`; il redirige vers la génération de la
|
||||
ligne d'achat propriétaire.
|
||||
|
||||
@@ -586,6 +586,103 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
self.assertEqual(values[0]['sale'], sale.id)
|
||||
self.assertEqual(values[0]['sale_line'], sale_line.id)
|
||||
|
||||
def test_purchase_pnl_fee_deduplicates_sale_lot_seen_twice(self):
|
||||
'purchase-side fee pnl does not duplicate a sale lot already in matching'
|
||||
Valuation = Pool().get('valuation.valuation')
|
||||
currency = Mock(id=1)
|
||||
unit = Mock(id=2)
|
||||
product = Mock(id=3, name='Broker commission')
|
||||
supplier = Mock(id=4)
|
||||
sale = Mock(id=5)
|
||||
sale_line = Mock(id=6, sale=sale, finished=False)
|
||||
sale_lot = Mock(id=7, sale_line=sale_line, lot_type='virtual')
|
||||
sale_lot.get_current_quantity_converted.return_value = Decimal('2')
|
||||
fee = Mock(
|
||||
product=product,
|
||||
supplier=supplier,
|
||||
type='budgeted',
|
||||
p_r='pay',
|
||||
mode='lumpsum',
|
||||
price=Decimal('10'),
|
||||
currency=currency,
|
||||
shipment_in=None,
|
||||
sale_line=sale_line,
|
||||
unit=unit,
|
||||
)
|
||||
line = Mock(
|
||||
id=9,
|
||||
finished=False,
|
||||
lots=[sale_lot],
|
||||
get_matched_lines=Mock(return_value=[Mock(lot_s=sale_lot)]),
|
||||
purchase=Mock(id=10, currency=currency),
|
||||
unit=unit,
|
||||
)
|
||||
fee_lots = Mock()
|
||||
fee_lots.search.return_value = [Mock(fee=fee)]
|
||||
|
||||
with patch('trytond.modules.purchase_trade.valuation.Pool') as PoolMock:
|
||||
PoolMock.return_value.get.side_effect = lambda name: {
|
||||
'ir.date': Mock(today=Mock(return_value=datetime.date(2026, 5, 14))),
|
||||
'currency.currency': Mock(),
|
||||
'fee.lots': fee_lots,
|
||||
'lot.qt': Mock(),
|
||||
}[name]
|
||||
|
||||
values = Valuation.create_pnl_fee_from_line(line)
|
||||
|
||||
self.assertEqual(len(values), 1)
|
||||
self.assertEqual(values[0]['type'], 'sale fee')
|
||||
self.assertEqual(values[0]['lot'], sale_lot.id)
|
||||
|
||||
def test_snapshot_identity_prefers_sale_line_over_purchase_line(self):
|
||||
'valuation snapshot identity ignores purchase line when sale line exists'
|
||||
Valuation = Pool().get('valuation.valuation')
|
||||
base = {
|
||||
'purchase': 1,
|
||||
'line': 2,
|
||||
'sale': 3,
|
||||
'sale_line': 4,
|
||||
'lot': 5,
|
||||
'type': 'sale fee',
|
||||
'reference': 'Broker/Open',
|
||||
'counterparty': 6,
|
||||
'product': 7,
|
||||
'state': 'budgeted',
|
||||
'strategy': None,
|
||||
'price': Decimal('10'),
|
||||
'quantity': Decimal('2'),
|
||||
'amount': Decimal('-20'),
|
||||
}
|
||||
regenerated = base.copy()
|
||||
regenerated.update({
|
||||
'purchase': None,
|
||||
'line': None,
|
||||
'price': Decimal('12'),
|
||||
'quantity': Decimal('3'),
|
||||
'amount': Decimal('-36'),
|
||||
})
|
||||
|
||||
self.assertEqual(
|
||||
Valuation._value_key(base),
|
||||
Valuation._value_key(regenerated))
|
||||
|
||||
def test_generate_from_matched_sale_line_does_not_create_snapshot(self):
|
||||
'matched sale line pnl is owned by the matched purchase line'
|
||||
Valuation = Pool().get('valuation.valuation')
|
||||
purchase_line = Mock(id=10)
|
||||
sale_line = Mock()
|
||||
sale_line.get_matched_lines.return_value = [
|
||||
Mock(lot_p=Mock(line=purchase_line))]
|
||||
|
||||
with patch.object(Valuation, '_delete_existing_sale_line') as delete_existing, patch.object(
|
||||
Valuation, 'create_pnl_fee_from_sale_line') as create_fees, patch.object(
|
||||
Valuation, 'generate') as generate:
|
||||
Valuation.generate_from_sale_line(sale_line, valuation_type='fees')
|
||||
|
||||
generate.assert_called_once_with(purchase_line, valuation_type='fees')
|
||||
delete_existing.assert_not_called()
|
||||
create_fees.assert_not_called()
|
||||
|
||||
def test_purchase_open_pnl_fee_skips_zero_virtual_lot(self):
|
||||
'purchase-side fee pnl ignores open virtual lots with no quantity'
|
||||
Valuation = Pool().get('valuation.valuation')
|
||||
|
||||
@@ -136,6 +136,22 @@ class ValuationBase(ModelSQL):
|
||||
values['sale'] = sale_line.sale.id
|
||||
values['sale_line'] = sale_line.id
|
||||
|
||||
@staticmethod
|
||||
def _record_id(record):
|
||||
return getattr(record, 'id', record)
|
||||
|
||||
@classmethod
|
||||
def _unique_lots(cls, lots):
|
||||
unique = []
|
||||
seen = set()
|
||||
for lot in lots:
|
||||
lot_id = cls._record_id(lot)
|
||||
if not lot or lot_id in seen:
|
||||
continue
|
||||
unique.append(lot)
|
||||
seen.add(lot_id)
|
||||
return tuple(unique)
|
||||
|
||||
@classmethod
|
||||
def _filter_values_by_types(cls, values, selected_types):
|
||||
if selected_types is None:
|
||||
@@ -166,6 +182,105 @@ class ValuationBase(ModelSQL):
|
||||
if valuation_lines:
|
||||
ValuationLine.delete(valuation_lines)
|
||||
|
||||
@classmethod
|
||||
def _value_key(cls, value):
|
||||
return (
|
||||
value.get('sale_line') or None,
|
||||
None if value.get('sale_line') else value.get('line'),
|
||||
value.get('lot'),
|
||||
value.get('type'),
|
||||
value.get('reference'),
|
||||
value.get('counterparty'),
|
||||
value.get('product'),
|
||||
value.get('state'),
|
||||
value.get('strategy'),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _dedupe_values(cls, values):
|
||||
unique = []
|
||||
seen = set()
|
||||
for value in values:
|
||||
key = cls._value_key(value)
|
||||
if key in seen:
|
||||
continue
|
||||
unique.append(value)
|
||||
seen.add(key)
|
||||
return unique
|
||||
|
||||
@classmethod
|
||||
def _snapshot_identity_domain(cls, value):
|
||||
domain = [
|
||||
('lot', '=', value.get('lot')),
|
||||
('type', '=', value.get('type')),
|
||||
('reference', '=', value.get('reference')),
|
||||
('counterparty', '=', value.get('counterparty')),
|
||||
('product', '=', value.get('product')),
|
||||
('state', '=', value.get('state')),
|
||||
('strategy', '=', value.get('strategy')),
|
||||
]
|
||||
if value.get('sale_line'):
|
||||
domain.append(('sale_line', '=', value.get('sale_line')))
|
||||
elif value.get('line'):
|
||||
domain.append(('line', '=', value.get('line')))
|
||||
elif value.get('sale'):
|
||||
domain.append(('sale', '=', value.get('sale')))
|
||||
elif value.get('purchase'):
|
||||
domain.append(('purchase', '=', value.get('purchase')))
|
||||
return domain
|
||||
|
||||
@classmethod
|
||||
def _delete_existing_snapshot_values(cls, values):
|
||||
if not values:
|
||||
return
|
||||
|
||||
Date = Pool().get('ir.date')
|
||||
Valuation = Pool().get('valuation.valuation')
|
||||
ValuationLine = Pool().get('valuation.valuation.line')
|
||||
seen = set()
|
||||
|
||||
for value in values:
|
||||
if not value.get('lot') or not value.get('type'):
|
||||
continue
|
||||
key = cls._value_key(value)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
|
||||
valuation_line_domain = cls._snapshot_identity_domain(value)
|
||||
valuation_domain = list(valuation_line_domain)
|
||||
valuation_domain.append(('date', '=', Date.today()))
|
||||
|
||||
valuations = Valuation.search(valuation_domain)
|
||||
if valuations:
|
||||
Valuation.delete(valuations)
|
||||
|
||||
valuation_lines = ValuationLine.search(valuation_line_domain)
|
||||
if valuation_lines:
|
||||
ValuationLine.delete(valuation_lines)
|
||||
|
||||
@classmethod
|
||||
def _matched_purchase_lines_from_sale_line(cls, sale_line):
|
||||
getter = getattr(sale_line, 'get_matched_lines', None)
|
||||
if not callable(getter):
|
||||
return []
|
||||
|
||||
purchase_lines = []
|
||||
seen = set()
|
||||
matched_lines = getter() or []
|
||||
try:
|
||||
iterator = iter(matched_lines)
|
||||
except TypeError:
|
||||
return []
|
||||
for matched_line in iterator:
|
||||
lot_p = getattr(matched_line, 'lot_p', None)
|
||||
line = getattr(lot_p, 'line', None)
|
||||
line_id = cls._record_id(line)
|
||||
if line and line_id not in seen:
|
||||
purchase_lines.append(line)
|
||||
seen.add(line_id)
|
||||
return purchase_lines
|
||||
|
||||
@classmethod
|
||||
def _delete_existing_sale_line(cls, sale_line, selected_types=None):
|
||||
Date = Pool().get('ir.date')
|
||||
@@ -748,7 +863,7 @@ class ValuationBase(ModelSQL):
|
||||
and not cls._ignore_finished_open_lot(line, s.lot_s)
|
||||
and not cls._ignore_finished_open_lot(s.lot_s.sale_line, s.lot_s)
|
||||
)
|
||||
all_lots = cls._valuation_lots(line) + sale_open_lots
|
||||
all_lots = cls._unique_lots(cls._valuation_lots(line) + sale_open_lots)
|
||||
for lot in all_lots:
|
||||
if cls._ignore_empty_open_fee_lot(lot):
|
||||
continue
|
||||
@@ -955,6 +1070,8 @@ class ValuationBase(ModelSQL):
|
||||
values.extend(cls.create_pnl_price_from_line(line))
|
||||
values.extend(cls.create_pnl_der_from_line(line))
|
||||
values = cls._filter_values_by_types(values, selected_types)
|
||||
values = cls._dedupe_values(values)
|
||||
cls._delete_existing_snapshot_values(values)
|
||||
|
||||
if values:
|
||||
Valuation = Pool().get('valuation.valuation')
|
||||
@@ -964,6 +1081,13 @@ class ValuationBase(ModelSQL):
|
||||
|
||||
@classmethod
|
||||
def generate_from_sale_line(cls, sale_line, valuation_type='all'):
|
||||
matched_purchase_lines = cls._matched_purchase_lines_from_sale_line(
|
||||
sale_line)
|
||||
if matched_purchase_lines:
|
||||
for line in matched_purchase_lines:
|
||||
cls.generate(line, valuation_type=valuation_type)
|
||||
return
|
||||
|
||||
selected_types = cls._get_generate_types(valuation_type)
|
||||
cls._delete_existing_sale_line(sale_line, selected_types=selected_types)
|
||||
values = []
|
||||
@@ -971,6 +1095,8 @@ class ValuationBase(ModelSQL):
|
||||
values.extend(cls.create_pnl_price_from_sale_line(sale_line))
|
||||
values.extend(cls.create_pnl_der_from_sale_line(sale_line))
|
||||
values = cls._filter_values_by_types(values, selected_types)
|
||||
values = cls._dedupe_values(values)
|
||||
cls._delete_existing_snapshot_values(values)
|
||||
|
||||
if values:
|
||||
Valuation = Pool().get('valuation.valuation')
|
||||
|
||||
Reference in New Issue
Block a user