diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py index c1f54ef..83d1af8 100755 --- a/modules/purchase_trade/lot.py +++ b/modules/purchase_trade/lot.py @@ -1025,6 +1025,24 @@ class LotQt( logger.info("MATCH_LOTS:%s",lot_s) LotQt = Pool().get('lot.qt') Lot = Pool().get('lot.lot') + for lp in lot_p: + qt_p = Decimal(str(lp.lot_matched_qt or 0)) + if qt_p <= 0: + continue + lqt = LotQt(lp.lot_r_id) + available = Decimal(str(lqt.lot_quantity or 0)) + if qt_p > available: + raise UserError( + "Quantity to match exceeds the available purchase quantity.") + for ls in lot_s: + qt_s = Decimal(str(ls.lot_matched_qt or 0)) + if qt_s <= 0: + continue + lqt = LotQt(ls.lot_r_id) + available = abs(Decimal(str(lqt.lot_quantity or 0))) + if qt_s > available: + raise UserError( + "Quantity to match exceeds the available sale quantity.") for lp in lot_p: qt_p = lp.lot_matched_qt if qt_p == 0: diff --git a/modules/purchase_trade/service.py b/modules/purchase_trade/service.py index 55348e3..abb373b 100644 --- a/modules/purchase_trade/service.py +++ b/modules/purchase_trade/service.py @@ -37,6 +37,7 @@ class ContractFactory: created = [] sources = cls._get_sources(ct, type_) + cls._validate_requested_quantity(contracts, sources, ct) base_contract = cls._get_base_contract(sources, ct, type_) for c in contracts: @@ -251,6 +252,18 @@ class ContractFactory: if getattr(line.unit, 'id', None) != getattr(first_line.unit, 'id', None): raise UserError('Selected lots must share the same unit.') + @classmethod + def _validate_requested_quantity(cls, contracts, sources, ct): + if not getattr(ct, 'matched', False) or not sources: + return + available = sum(source['quantity'] for source in sources) + requested = sum( + cls._normalize_quantity(contract.quantity) + for contract in contracts) + if requested > available: + raise UserError( + 'The requested quantity exceeds the selected open quantity.') + @classmethod def _get_line_sources(cls, contract_detail, sources, ct): if not ct.matched or len(sources) <= 1: diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 29c9652..b0431c9 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -2348,6 +2348,36 @@ class PurchaseTradeTestCase(ModuleTestCase): with self.assertRaises(UserError): ContractFactory._get_line_sources(contract_detail, sources, ct) + def test_contract_factory_rejects_total_above_selected_open_quantity(self): + 'matched create contracts cannot consume more than selected open quantity' + contracts = [ + Mock(quantity=Decimal('100')), + Mock(quantity=Decimal('100')), + ] + ct = Mock(matched=True) + sources = [ + {'lot': Mock(), 'trade_line': Mock(), 'quantity': Decimal('100')}, + ] + + with self.assertRaises(UserError): + ContractFactory._validate_requested_quantity(contracts, sources, ct) + + def test_lot_matching_rejects_purchase_quantity_above_available(self): + 'apply matching cannot consume more than available purchase quantity' + class LotQtMock: + def __call__(self, _id): + return Mock(lot_quantity=Decimal('100')) + + pool = Mock() + pool.get.side_effect = ( + lambda name: LotQtMock() if name == 'lot.qt' else Mock()) + purchase_lot = Mock( + lot_matched_qt=Decimal('101'), lot_r_id=1) + + with patch.object(lot_module, 'Pool', return_value=pool): + with self.assertRaises(UserError): + lot_module.LotQt.match_lots([purchase_lot], []) + def test_contract_detail_defaults_sale_locations_from_dropship_purchase(self): 'create contracts copies supplier-to-customer locations from purchase to sale' supplier = Mock(id=1, type='supplier')