From 83b879d200daa6849a70dc3ec77c05ba44257afb Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Tue, 23 Jun 2026 14:31:52 +0200 Subject: [PATCH] Lot invoicing + Fee padding --- modules/purchase_trade/fee.py | 15 ++++- modules/purchase_trade/purchase.py | 6 +- modules/purchase_trade/tests/test_module.py | 71 +++++++++++++++++++++ 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/modules/purchase_trade/fee.py b/modules/purchase_trade/fee.py index 06b79b1..fce5a0c 100755 --- a/modules/purchase_trade/fee.py +++ b/modules/purchase_trade/fee.py @@ -393,9 +393,16 @@ class Fee(ModelSQL,ModelView): if fee.purchase: fl = FeeLots.search([('fee','=',fee.id)]) logger.info("PROCESS_FROM_FEE:%s",fl) - fee.adjust_purchase_values() - invoices = Purchase._process_invoice( - [fee.purchase], [e.lot for e in fl], 'service') + if was_invoiced: + with Transaction().set_context( + _purchase_trade_ignore_fee_padding=True): + fee.adjust_purchase_values() + invoices = Purchase._process_invoice( + [fee.purchase], [e.lot for e in fl], 'service') + else: + fee.adjust_purchase_values() + invoices = Purchase._process_invoice( + [fee.purchase], [e.lot for e in fl], 'service') invoice = invoices.get(fee.purchase) if invoices else None logger.info( "PROCESS_FROM_FEE_RESULT: fee=%s purchase=%s invoice=%s " @@ -678,6 +685,8 @@ class Fee(ModelSQL,ModelView): return self._convert_padding_quantity(padding, source_unit) def get_padding_quantity(self): + if Transaction().context.get('_purchase_trade_ignore_fee_padding'): + return Decimal(0) if not getattr(self, 'add_padding', False): return Decimal(0) return sum( diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index 477f53c..3a130a2 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -1960,7 +1960,11 @@ class Line(metaclass=PoolMeta): invoice_line.quantity = 1 elif fee.mode == 'ppack': invoice_line.quantity = fee.quantity - elif fee.mode == 'perqt' and getattr(fee, 'add_padding', False): + elif ( + fee.mode == 'perqt' + and getattr(fee, 'add_padding', False) + and not Transaction().context.get( + '_purchase_trade_ignore_fee_padding')): invoice_line.quantity = fee.get_billing_quantity() else: invoice_line.quantity = fee.get_fee_lots_qt() diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 7a6a391..adb2760 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -3105,6 +3105,36 @@ class PurchaseTradeTestCase(ModuleTestCase): fee, '_get_effective_fee_lots', return_value=[lot]): self.assertEqual(fee.get_amount(), Decimal('14.00')) + def test_fee_dn_cn_amount_ignores_padding_for_current_value(self): + 'fee DN/CN current value ignores padding while reversal keeps prior line' + Fee = Pool().get('fee.fee') + fee = Fee() + unit = SimpleNamespace(id=1) + sale_line = SimpleNamespace(unit=unit, unit_price=Decimal('100')) + lot = SimpleNamespace( + id=1, + lot_type='physic', + sale_line=sale_line, + sale_invoice_padding=Decimal('2'), + lot_hist=[], + get_current_quantity_converted=( + lambda state=0, unit=None: Decimal('5'))) + fee.mode = 'pprice' + fee.price = Decimal('2') + fee.add_padding = True + fee.unit = unit + fee.quantity = Decimal('5') + fee.line = None + fee.sale_line = sale_line + fee.shipment_in = None + + with patch.object( + fee, '_get_effective_fee_lots', return_value=[lot]): + self.assertEqual(fee.get_amount(), Decimal('14.00')) + with Transaction().set_context( + _purchase_trade_ignore_fee_padding=True): + self.assertEqual(fee.get_amount(), Decimal('10.00')) + def test_percent_price_fee_warns_when_line_lots_are_partial(self): '% price fee warns when it does not cover all line lots' Fee = Pool().get('fee.fee') @@ -3238,6 +3268,47 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(invoice_line.quantity, Decimal('7')) fee.get_billing_quantity.assert_called_once_with() + def test_purchase_trade_service_fee_dn_cn_ignores_padding_quantity(self): + 'fee DN/CN current invoice line uses final lot quantity without padding' + Line = purchase_module.Line + line = Line() + line.purchase = Mock(id=20) + lot = Mock(id=30) + invoice_line = Mock(quantity=Decimal('7'), unit_price=Decimal('10')) + fee = Mock( + state='invoiced', + mode='perqt', + add_padding=True, + warn_percent_price_partial_lots=Mock(), + get_billing_quantity=Mock(return_value=Decimal('7')), + get_fee_lots_qt=Mock(return_value=Decimal('5'))) + fee_model = Mock() + fee_model.search.return_value = [fee] + + with patch( + 'trytond.modules.purchase_trade.purchase.Pool' + ) as PoolMock, patch.object( + Line, '_get_last_fee_invoice_line', + return_value=Mock( + quantity=Decimal('7'), + unit_price=Decimal('10'), + invoice=Mock(party=Mock()))), patch.object( + Line, '_get_fee_reversal_invoice_line', + return_value=Mock()): + PoolMock.return_value.get.side_effect = lambda name: { + 'fee.fee': fee_model, + }[name] + + with Transaction().set_context( + _purchase_trade_ignore_fee_padding=True): + result = line._get_service_fee_invoice_lines( + invoice_line, lot) + + self.assertEqual(result, [ANY, invoice_line]) + self.assertEqual(invoice_line.quantity, Decimal('5')) + fee.get_billing_quantity.assert_not_called() + fee.get_fee_lots_qt.assert_called_once_with() + def test_lot_invoice_sale_padding_refreshes_per_quantity_fee_amount(self): 'sale padding refreshes displayed per quantity fee amount' start = lot_module.LotInvoiceStart()