From 03d65c253c4a20a0f8f0f65d43cae987d42f2e35 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Thu, 30 Apr 2026 13:16:45 +0200 Subject: [PATCH] Bug Pnl lot physic --- modules/purchase_trade/docs/business-rules.md | 2 + modules/purchase_trade/tests/test_module.py | 58 +++++++++++++++++++ modules/purchase_trade/valuation.py | 12 ++++ 3 files changed, 72 insertions(+) diff --git a/modules/purchase_trade/docs/business-rules.md b/modules/purchase_trade/docs/business-rules.md index 3d896cb..331f947 100644 --- a/modules/purchase_trade/docs/business-rules.md +++ b/modules/purchase_trade/docs/business-rules.md @@ -126,6 +126,8 @@ Owner technique: `a completer` `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: diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 3bb2b4c..e7ca299 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -474,6 +474,64 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(values[0]['sale'], sale.id) self.assertEqual(values[0]['sale_line'], sale_line.id) + 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') + currency = Mock(id=1) + unit = Mock(id=2) + purchase_lot = Mock(id=8, sale_line=None, lot_type='virtual') + purchase_lot.get_current_quantity_converted.return_value = Decimal('0') + line = Mock( + id=9, + finished=False, + lots=[purchase_lot], + get_matched_lines=Mock(return_value=[]), + purchase=Mock(id=10, currency=currency), + unit=unit, + ) + fee_lots = Mock() + + 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, 4, 29))), + 'currency.currency': Mock(), + 'fee.lots': fee_lots, + 'lot.qt': Mock(), + }[name] + + values = Valuation.create_pnl_fee_from_line(line) + + self.assertEqual(values, []) + fee_lots.search.assert_not_called() + + def test_sale_open_pnl_fee_skips_zero_virtual_lot(self): + 'sale-side fee pnl ignores open virtual lots with no quantity' + Valuation = Pool().get('valuation.valuation') + currency = Mock(id=1) + unit = Mock(id=2) + sale_lot = Mock(id=7, lot_type='virtual') + sale_lot.get_current_quantity_converted.return_value = Decimal('0') + sale_line = Mock( + id=6, + finished=False, + lots=[sale_lot], + sale=Mock(id=5, currency=currency), + unit=unit, + ) + fee_lots = Mock() + + 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, 4, 29))), + 'currency.currency': Mock(), + 'fee.lots': fee_lots, + }[name] + + values = Valuation.create_pnl_fee_from_sale_line(sale_line) + + self.assertEqual(values, []) + fee_lots.search.assert_not_called() + def test_sale_report_crop_name_handles_missing_crop(self): 'sale report crop name returns an empty string when crop is missing' Sale = Pool().get('sale.sale') diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py index 6f26120..6999f53 100644 --- a/modules/purchase_trade/valuation.py +++ b/modules/purchase_trade/valuation.py @@ -91,6 +91,14 @@ class ValuationBase(ModelSQL): def _ignore_finished_open_lot(cls, line, lot): return cls._is_finished(line) and lot.lot_type != 'physic' + @classmethod + def _lot_quantity(cls, lot): + return Decimal(str(lot.get_current_quantity_converted() or 0)) + + @classmethod + def _ignore_empty_open_fee_lot(cls, lot): + return lot.lot_type != 'physic' and cls._lot_quantity(lot) == 0 + @classmethod def _get_matched_sale_line_from_purchase_lot(cls, lot, LotQt=None): sale_line = getattr(lot, 'sale_line', None) @@ -707,6 +715,8 @@ class ValuationBase(ModelSQL): ) all_lots = cls._valuation_lots(line) + sale_open_lots for lot in all_lots: + if cls._ignore_empty_open_fee_lot(lot): + continue if lot.sale_line and cls._ignore_finished_open_lot(lot.sale_line, lot): continue matched_sale_line = cls._get_matched_sale_line_from_purchase_lot( @@ -768,6 +778,8 @@ class ValuationBase(ModelSQL): FeeLots = Pool().get('fee.lots') for lot in cls._valuation_lots(sale_line): + if cls._ignore_empty_open_fee_lot(lot): + continue fl = FeeLots.search([('lot', '=', lot.id)]) if not fl: continue