Bug pnl fee

This commit is contained in:
2026-05-14 14:31:23 +02:00
parent 82a30f246d
commit 20a4e20e91
8 changed files with 603 additions and 36 deletions

View File

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

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

View File

@@ -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&#x27;achat liée.
</li>
<li style="margin:0.38rem 0;">Le PnL d&#x27;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&#x27;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&#x27;achat propriétaire.
</li>
</ul>

View File

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

View File

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

View File

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

View File

@@ -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')

View File

@@ -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')