Add constraint delete matched contracts

This commit is contained in:
2026-04-20 10:29:33 +02:00
parent 8906f00d36
commit 897c6f6824
3 changed files with 80 additions and 6 deletions

View File

@@ -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:

View File

@@ -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']

View File

@@ -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')