From 897c6f6824413f65dd26ed4ad965d64b9b915739 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Mon, 20 Apr 2026 10:29:33 +0200 Subject: [PATCH] Add constraint delete matched contracts --- modules/purchase_trade/purchase.py | 31 ++++++++++++++---- modules/purchase_trade/sale.py | 19 +++++++++++ modules/purchase_trade/tests/test_module.py | 36 +++++++++++++++++++++ 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index 99d8ca8..154f642 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -366,12 +366,31 @@ class Purchase(metaclass=PoolMeta): def default_tol_min(cls): return 0 - @classmethod - def default_tol_max(cls): - return 0 - - @property - def report_terms(self): + @classmethod + def default_tol_max(cls): + return 0 + + @staticmethod + def _has_matched_physical_lots(purchase): + for line in purchase.lines or []: + for lot in line.lots or []: + if ( + getattr(lot, 'lot_type', None) == 'physic' + and getattr(lot, 'line', None) + and getattr(lot, 'sale_line', None)): + return True + return False + + @classmethod + def delete(cls, purchases): + for purchase in purchases: + if cls._has_matched_physical_lots(purchase): + raise UserError( + "You cannot delete a purchase matched to a sale") + super().delete(purchases) + + @property + def report_terms(self): if self.lines: return self.lines[0].note else: diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py index 5941e91..2cca027 100755 --- a/modules/purchase_trade/sale.py +++ b/modules/purchase_trade/sale.py @@ -334,6 +334,25 @@ class Sale(metaclass=PoolMeta): def default_tol_max(cls): return 0 + @staticmethod + def _has_matched_physical_lots(sale): + for line in sale.lines or []: + for lot in line.lots or []: + if ( + getattr(lot, 'lot_type', None) == 'physic' + and getattr(lot, 'line', None) + and getattr(lot, 'sale_line', None)): + return True + return False + + @classmethod + def delete(cls, sales): + for sale in sales: + if cls._has_matched_physical_lots(sale): + raise UserError( + "You cannot delete a sale matched to a purchase") + super().delete(sales) + def _get_report_lines(self): return [line for line in self.lines if getattr(line, 'type', None) == 'line'] diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 0d8d5b1..8d2fe54 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -185,6 +185,42 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual( PurchaseLine.default_pricing_rule(), 'Default pricing rule') + def test_purchase_delete_blocks_matched_physical_contract(self): + 'purchase delete stops when a physical lot already links purchase and sale' + Purchase = Pool().get('purchase.purchase') + matched_lot = Mock(lot_type='physic', line=Mock(), sale_line=Mock()) + purchase = Mock(lines=[Mock(lots=[matched_lot])]) + + with self.assertRaises(UserError): + Purchase.delete([purchase]) + + def test_purchase_delete_delegates_when_no_matched_physical_lot(self): + 'purchase delete keeps default flow when no matched physical lot exists' + Purchase = Pool().get('purchase.purchase') + purchase = Mock(lines=[Mock(lots=[Mock(lot_type='virtual')])]) + + with patch('trytond.modules.purchase_trade.purchase.super') as super_mock: + Purchase.delete([purchase]) + super_mock.return_value.delete.assert_called_once_with([purchase]) + + def test_sale_delete_blocks_matched_physical_contract(self): + 'sale delete stops when a physical lot already links sale and purchase' + Sale = Pool().get('sale.sale') + matched_lot = Mock(lot_type='physic', line=Mock(), sale_line=Mock()) + sale = Mock(lines=[Mock(lots=[matched_lot])]) + + with self.assertRaises(UserError): + Sale.delete([sale]) + + def test_sale_delete_delegates_when_no_matched_physical_lot(self): + 'sale delete keeps default flow when no matched physical lot exists' + Sale = Pool().get('sale.sale') + sale = Mock(lines=[Mock(lots=[Mock(lot_type='virtual')])]) + + with patch('trytond.modules.purchase_trade.sale.super') as super_mock: + Sale.delete([sale]) + super_mock.return_value.delete.assert_called_once_with([sale]) + def test_component_quota_uses_quantity_fallback_when_theoretical_is_missing(self): 'component quota does not crash when theoretical quantity is still empty' SaleComponent = Pool().get('pricing.component')