From 4bbd7a5e76340201cfdc5554742c37121681f39b Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Thu, 9 Apr 2026 21:38:26 +0200 Subject: [PATCH] bug template --- modules/purchase_trade/invoice.py | 49 ++++++++++++++++++ modules/purchase_trade/tests/test_module.py | 57 +++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/modules/purchase_trade/invoice.py b/modules/purchase_trade/invoice.py index 05154fc..c6fead5 100644 --- a/modules/purchase_trade/invoice.py +++ b/modules/purchase_trade/invoice.py @@ -109,6 +109,9 @@ class Invoice(metaclass=PoolMeta): def _get_report_invoice_line_weights(self, line): lots = self._get_report_preferred_lots(line) + if lots and self._report_invoice_line_reuses_lot(line): + quantity = self._get_report_invoice_line_quantity_from_line(line) + return quantity, quantity if lots: sign = self._get_report_line_sign(line) net_total = Decimal(0) @@ -124,6 +127,49 @@ class Invoice(metaclass=PoolMeta): quantity = Decimal(str(getattr(line, 'quantity', 0) or 0)) return quantity, quantity + @staticmethod + def _get_report_line_lot_keys(line): + keys = [] + for lot in Invoice._get_report_preferred_lots(line): + lot_id = getattr(lot, 'id', None) + keys.append(lot_id if lot_id is not None else id(lot)) + return tuple(sorted(keys)) + + def _report_invoice_line_reuses_lot(self, line): + line_keys = self._get_report_line_lot_keys(line) + if not line_keys: + return False + for other in self._get_report_invoice_lines(): + if other is line: + continue + if self._get_report_line_lot_keys(other) == line_keys: + return True + return False + + @staticmethod + def _convert_report_quantity(quantity, from_unit, to_unit): + value = Decimal(str(quantity or 0)) + if not from_unit or not to_unit: + return value + if getattr(from_unit, 'id', None) == getattr(to_unit, 'id', None): + return value + from_name = getattr(from_unit, 'rec_name', None) + to_name = getattr(to_unit, 'rec_name', None) + if from_name and to_name and from_name == to_name: + return value + converted = Pool().get('product.uom').compute_qty( + from_unit, float(value), to_unit) or 0 + return Decimal(str(converted)) + + @classmethod + def _get_report_invoice_line_quantity_from_line(cls, line): + quantity = Decimal(str(getattr(line, 'quantity', 0) or 0)) + return cls._convert_report_quantity( + quantity, + getattr(line, 'unit', None), + cls._get_report_invoice_line_unit(line), + ) + @staticmethod def _get_report_invoice_line_unit(line): lots = Invoice._get_report_preferred_lots(line) @@ -1022,6 +1068,9 @@ class InvoiceLine(metaclass=PoolMeta): @property def report_net(self): if self.type == 'line': + invoice = getattr(self, 'invoice', None) + if invoice and invoice._report_invoice_line_reuses_lot(self): + return Invoice._get_report_invoice_line_quantity_from_line(self) lot = getattr(self, 'lot', None) if lot: net, _ = Invoice._get_report_lot_hist_weights(lot) diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index da080b8..f6ec348 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -1183,6 +1183,63 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(invoice.report_net, Decimal('800')) self.assertEqual(invoice.report_gross, Decimal('820')) + def test_invoice_report_uses_line_quantities_when_same_lot_is_invoiced_twice(self): + 'invoice final note keeps line differences when two lines share the same lot' + Invoice = Pool().get('account.invoice') + + mt = Mock(id=1, rec_name='MT') + kg = Mock(id=2, rec_name='KILOGRAM') + shared_lot = Mock(id=10, lot_type='physic', lot_unit_line=kg) + shared_lot.get_hist_quantity.return_value = ( + Decimal('999995'), + Decimal('999995'), + ) + + negative = Mock( + type='line', + quantity=Decimal('-999.995'), + unit=mt, + lot=shared_lot, + ) + positive = Mock( + type='line', + quantity=Decimal('999.990'), + unit=mt, + lot=shared_lot, + ) + + invoice = Invoice() + negative.invoice = invoice + positive.invoice = invoice + invoice.lines = [negative, positive] + + uom_model = Mock() + uom_model.search.return_value = [Mock(id=3, rec_name='LBS', symbol='LBS')] + uom_model.compute_qty.side_effect = ( + lambda from_unit, qty, to_unit: ( + qty * 1000 + if getattr(from_unit, 'rec_name', None) == 'MT' + and getattr(to_unit, 'rec_name', None) == 'KILOGRAM' + else ( + qty * 2.20462 + if getattr(from_unit, 'rec_name', None) == 'KILOGRAM' + and getattr(to_unit, 'rec_name', None) == 'LBS' + else qty + ) + ) + ) + + with patch('trytond.modules.purchase_trade.invoice.Pool') as PoolMock: + PoolMock.return_value.get.return_value = uom_model + + self.assertEqual(invoice.report_net, Decimal('-5.000')) + self.assertEqual( + invoice.report_quantity_lines.splitlines(), + [ + '-999995.0 KILOGRAM (-2204608.98 LBS)', + '999990.0 KILOGRAM (2204597.95 LBS)', + ]) + def test_invoice_report_weights_use_single_virtual_lot_when_no_physical(self): 'invoice uses the unique virtual lot hist when no physical lot exists' Invoice = Pool().get('account.invoice')