diff --git a/modules/purchase_trade/docs/business/INDEX.md b/modules/purchase_trade/docs/business/INDEX.md
index 8764737..0b6f923 100644
--- a/modules/purchase_trade/docs/business/INDEX.md
+++ b/modules/purchase_trade/docs/business/INDEX.md
@@ -19,7 +19,7 @@ Statut: `migration partielle`
Fees, freight, lots effectifs, % rate: fees.md
-Valuation, PnL, MTM, derivatives: valuation-pnl-mtm.md
+Valuation, PnL, MTM, derivatives: FR / EN
Factures provisoires/finales, padding: invoicing.md
@@ -78,6 +78,8 @@ Statut: `migration partielle`
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.
diff --git a/modules/purchase_trade/docs/business/valuation-pnl-mtm.en.md b/modules/purchase_trade/docs/business/valuation-pnl-mtm.en.md
new file mode 100644
index 0000000..795c213
--- /dev/null
+++ b/modules/purchase_trade/docs/business/valuation-pnl-mtm.en.md
@@ -0,0 +1,132 @@
+
+
+# Valuation, PnL, MTM
+
+Status: `partial migration`
+
+Language: `en`
+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.
+
+
diff --git a/modules/purchase_trade/docs/business/valuation-pnl-mtm.md b/modules/purchase_trade/docs/business/valuation-pnl-mtm.md
index 4e9e446..65c4dcf 100644
--- a/modules/purchase_trade/docs/business/valuation-pnl-mtm.md
+++ b/modules/purchase_trade/docs/business/valuation-pnl-mtm.md
@@ -4,39 +4,42 @@
Statut: `migration partielle`
+Langue: `fr`
+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 et derivative si applicable.
+ - 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.
+
- Ne pas attacher arbitrairement une sale unique si plusieurs sales sont 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, Sale/Physic.
+ - Références autorisées:
Purchase/Open, Purchase/Physic, Sale/Open, Sale/Physic.
- Un lot virtuel ne doit pas sortir avec une reference physique.
@@ -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
-- 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.
+
+
diff --git a/modules/purchase_trade/docs_source/business/INDEX.md b/modules/purchase_trade/docs_source/business/INDEX.md
index af2d99d..37aa87a 100644
--- a/modules/purchase_trade/docs_source/business/INDEX.md
+++ b/modules/purchase_trade/docs_source/business/INDEX.md
@@ -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.
diff --git a/modules/purchase_trade/docs_source/business/valuation-pnl-mtm.en.md b/modules/purchase_trade/docs_source/business/valuation-pnl-mtm.en.md
new file mode 100644
index 0000000..7c0982c
--- /dev/null
+++ b/modules/purchase_trade/docs_source/business/valuation-pnl-mtm.en.md
@@ -0,0 +1,95 @@
+# Valuation, PnL, MTM
+
+Status: `partial migration`
+
+Language: `en`
+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.
diff --git a/modules/purchase_trade/docs_source/business/valuation-pnl-mtm.md b/modules/purchase_trade/docs_source/business/valuation-pnl-mtm.md
index 073441a..325f8af 100644
--- a/modules/purchase_trade/docs_source/business/valuation-pnl-mtm.md
+++ b/modules/purchase_trade/docs_source/business/valuation-pnl-mtm.md
@@ -2,36 +2,39 @@
Statut: `migration partielle`
+Langue: `fr`
+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.
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index 9724908..7cd9fb3 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -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')
diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py
index 34f7171..1c490d1 100644
--- a/modules/purchase_trade/valuation.py
+++ b/modules/purchase_trade/valuation.py
@@ -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')