From 9677d0dae25317910571966aaee5c640afce55bb Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Thu, 14 May 2026 11:34:47 +0200 Subject: [PATCH] Bug quantity/th qt --- modules/purchase_trade/purchase.py | 23 +++++++- modules/purchase_trade/sale.py | 23 +++++++- modules/purchase_trade/tests/test_module.py | 65 +++++++++++++++++++-- 3 files changed, 103 insertions(+), 8 deletions(-) diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index b95288c..3964723 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -1147,6 +1147,25 @@ class Line(metaclass=PoolMeta): line.quantity = Decimal(str(quantity)).quantize(Decimal("0.00001")) return True + @classmethod + def _sync_quantity_counter_from_theorical(cls, line): + if cls._has_physical_lot(line): + return False + quantity = getattr(line, 'quantity_theorical', None) + if cls._is_empty_quantity(quantity): + quantity = Decimal(0) + else: + quantity = Decimal(str(quantity)).quantize(Decimal("0.00001")) + current = getattr(line, 'quantity', None) + try: + current = Decimal(str(current or 0)).quantize(Decimal("0.00001")) + except Exception: + current = Decimal(0) + if current == quantity: + return False + line.quantity = quantity + return True + @classmethod def _set_initial_quantity_values(cls, values): if 'quantity_theorical' not in values: @@ -1356,7 +1375,7 @@ class Line(metaclass=PoolMeta): 'quantity_theorical', 'quantity', 'lots', methods=['_recompute_trade_price_fields']) def on_change_quantity_theorical(self): - if self._sync_initial_quantity_from_theorical(self): + if self._sync_quantity_counter_from_theorical(self): self._recompute_trade_price_fields() @classmethod @@ -1632,7 +1651,7 @@ class Line(metaclass=PoolMeta): lines = sum(args[::2], []) for line in lines: if line.id in old_values: - if cls._sync_initial_quantity_from_theorical(line): + if cls._sync_quantity_counter_from_theorical(line): cls.save([line]) new = Decimal(line.quantity_theorical or 0) virtual_lots = [lot for lot in (line.lots or []) if lot.lot_type == 'virtual'] diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py index ad76568..15b44cc 100755 --- a/modules/purchase_trade/sale.py +++ b/modules/purchase_trade/sale.py @@ -1106,6 +1106,25 @@ class SaleLine(metaclass=PoolMeta): line.quantity = Decimal(str(quantity)).quantize(Decimal("0.00001")) return True + @classmethod + def _sync_quantity_counter_from_theorical(cls, line): + if cls._has_physical_lot(line): + return False + quantity = getattr(line, 'quantity_theorical', None) + if cls._is_empty_quantity(quantity): + quantity = Decimal(0) + else: + quantity = Decimal(str(quantity)).quantize(Decimal("0.00001")) + current = getattr(line, 'quantity', None) + try: + current = Decimal(str(current or 0)).quantize(Decimal("0.00001")) + except Exception: + current = Decimal(0) + if current == quantity: + return False + line.quantity = quantity + return True + @classmethod def _set_initial_quantity_values(cls, values): if 'quantity_theorical' not in values: @@ -1343,7 +1362,7 @@ class SaleLine(metaclass=PoolMeta): 'quantity_theorical', 'quantity', 'lots', methods=['_recompute_trade_price_fields']) def on_change_quantity_theorical(self): - if self._sync_initial_quantity_from_theorical(self): + if self._sync_quantity_counter_from_theorical(self): self._recompute_trade_price_fields() @classmethod @@ -1746,7 +1765,7 @@ class SaleLine(metaclass=PoolMeta): for line in lines: if line.id not in old_values: continue - if cls._sync_initial_quantity_from_theorical(line): + if cls._sync_quantity_counter_from_theorical(line): cls.save([line]) if old_values[line.id] is None: continue diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index c79170c..ca8127f 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -1503,7 +1503,9 @@ class PurchaseTradeTestCase(ModuleTestCase): 'sale line write increases virtual lot and open lot.qt when contractual qty grows' SaleLine = Pool().get('sale.line') line = Mock(id=1, quantity_theorical=Decimal('10')) + line.quantity = Decimal('10') line.unit = Mock() + line.fees = [] vlot = Mock(id=99, lot_type='virtual') vlot.get_current_quantity_converted.return_value = Decimal('10') line.lots = [vlot] @@ -1517,7 +1519,7 @@ class PurchaseTradeTestCase(ModuleTestCase): 'trytond.modules.purchase_trade.sale.Pool' ) as PoolMock, patch( 'trytond.modules.purchase_trade.sale.super' - ) as super_mock: + ) as super_mock, patch.object(SaleLine, 'save'): PoolMock.return_value.get.side_effect = lambda name: { 'lot.lot': lot_model, 'lot.qt': lotqt_model, @@ -1543,6 +1545,8 @@ class PurchaseTradeTestCase(ModuleTestCase): 'sale line write blocks contractual qty decrease when open lot.qt is insufficient' SaleLine = Pool().get('sale.line') line = Mock(id=2, quantity_theorical=Decimal('10')) + line.quantity = Decimal('10') + line.fees = [] vlot = Mock(id=100, lot_type='virtual') vlot.get_current_quantity_converted.return_value = Decimal('10') line.lots = [vlot] @@ -1557,7 +1561,7 @@ class PurchaseTradeTestCase(ModuleTestCase): 'trytond.modules.purchase_trade.sale.Pool' ) as PoolMock, patch( 'trytond.modules.purchase_trade.sale.super' - ) as super_mock: + ) as super_mock, patch.object(SaleLine, 'save'): PoolMock.return_value.get.side_effect = lambda name: { 'lot.lot': lot_model, 'lot.qt': lotqt_model, @@ -1578,7 +1582,9 @@ class PurchaseTradeTestCase(ModuleTestCase): 'purchase line write keeps open lot.qt net of existing physical lots' PurchaseLine = Pool().get('purchase.line') line = Mock(id=4, quantity_theorical=Decimal('10000')) + line.quantity = Decimal('10000') line.unit = Mock() + line.fees = [] vlot = Mock(id=102, lot_type='virtual') vlot.get_current_quantity_converted.return_value = Decimal('10000') physical = Mock(lot_type='physic') @@ -1594,7 +1600,7 @@ class PurchaseTradeTestCase(ModuleTestCase): 'trytond.modules.purchase_trade.purchase.Pool' ) as PoolMock, patch( 'trytond.modules.purchase_trade.purchase.super' - ) as super_mock: + ) as super_mock, patch.object(PurchaseLine, 'save'): PoolMock.return_value.get.side_effect = lambda name: { 'lot.lot': lot_model, 'lot.qt': lotqt_model, @@ -1612,15 +1618,65 @@ class PurchaseTradeTestCase(ModuleTestCase): [line], {'quantity_theorical': Decimal('20000')}) self.assertEqual(lqt.lot_quantity, Decimal('10000')) + self.assertEqual(line.quantity, Decimal('10000')) vlot.set_current_quantity.assert_not_called() lot_model.save.assert_not_called() lotqt_model.save.assert_not_called() + def test_purchase_line_write_syncs_quantity_without_physical_lots(self): + 'purchase line write keeps quantity equal to contractual qty without physical lots' + PurchaseLine = Pool().get('purchase.line') + line = Mock(id=6, quantity_theorical=Decimal('1500')) + line.quantity = Decimal('1500') + line.unit = Mock() + line.fees = [] + vlot = Mock(id=104, lot_type='virtual') + vlot.get_current_quantity_converted.return_value = Decimal('1500') + line.lots = [vlot] + free_lqt = Mock(lot_quantity=Decimal('50')) + allocated_lqts = [ + Mock(lot_quantity=Decimal('400')), + Mock(lot_quantity=Decimal('500')), + Mock(lot_quantity=Decimal('550')), + ] + + lot_model = Mock() + lotqt_model = Mock() + lotqt_model.search.side_effect = [[free_lqt], allocated_lqts] + + with patch( + 'trytond.modules.purchase_trade.purchase.Pool' + ) as PoolMock, patch( + 'trytond.modules.purchase_trade.purchase.super' + ) as super_mock, patch.object(PurchaseLine, 'save') as save: + PoolMock.return_value.get.side_effect = lambda name: { + 'lot.lot': lot_model, + 'lot.qt': lotqt_model, + }[name] + + def fake_super_write(*args): + for records, values in zip(args[::2], args[1::2]): + if 'quantity_theorical' in values: + for record in records: + record.quantity_theorical = values['quantity_theorical'] + + super_mock.return_value.write.side_effect = fake_super_write + + PurchaseLine.write( + [line], {'quantity_theorical': Decimal('2000')}) + + self.assertEqual(line.quantity, Decimal('2000.00000')) + self.assertEqual(free_lqt.lot_quantity, Decimal('550.00000')) + vlot.set_current_quantity.assert_called_once_with( + Decimal('2000.00000'), Decimal('2000.00000'), 1) + save.assert_called() + def test_purchase_line_write_syncs_virtual_fee_quantity(self): 'purchase line write updates fee quantity when only virtual lot exists' PurchaseLine = Pool().get('purchase.line') fee = Mock() line = Mock(id=5, quantity_theorical=Decimal('20000')) + line.quantity = Decimal('20000') line.unit = Mock() line.fees = [fee] vlot = Mock(id=103, lot_type='virtual') @@ -1636,7 +1692,7 @@ class PurchaseTradeTestCase(ModuleTestCase): 'trytond.modules.purchase_trade.purchase.Pool' ) as PoolMock, patch( 'trytond.modules.purchase_trade.purchase.super' - ) as super_mock: + ) as super_mock, patch.object(PurchaseLine, 'save'): PoolMock.return_value.get.side_effect = lambda name: { 'lot.lot': lot_model, 'lot.qt': lotqt_model, @@ -1661,6 +1717,7 @@ class PurchaseTradeTestCase(ModuleTestCase): 'purchase line write does not re-add quantity when initializing contractual qty' PurchaseLine = Pool().get('purchase.line') line = Mock(id=3, quantity_theorical=None) + line.quantity = Decimal('10') vlot = Mock(id=101, lot_type='virtual') vlot.get_current_quantity_converted.return_value = Decimal('10') line.lots = [vlot]