diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 98b849a94f6..a018dc1f1fb 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -133,6 +133,7 @@ class Opportunity(TransactionBase, CRMNote): self.validate_uom_is_integer("uom", "qty") self.validate_cust_name() self.validate_party() + self.validate_party_specific_items() self.map_fields() self.validate_qty() self.set_exchange_rate() @@ -145,6 +146,32 @@ class Opportunity(TransactionBase, CRMNote): def on_update(self): self.update_prospect() + def validate_party_specific_items(self): + if self.opportunity_from != "Customer" or not self.party_name: + return + + from erpnext.selling.doctype.party_specific_item.party_specific_item import get_party_specific_items + + allowed_values = get_party_specific_items(self.party_name) + if not allowed_values: + return + + rows = [row for row in self.get("items") if row.item_code] + allowed_items = frappe.get_all( + "Item", + filters=[["name", "in", list({row.item_code for row in rows})]] + + [[field, "in", values] for field, values in allowed_values.items()], + pluck="name", + ) + for row in rows: + if row.item_code not in allowed_items: + frappe.throw( + _("Row {0}: Item {1} is not allowed for {2} {3}.").format( + row.idx, frappe.bold(row.item_code), _("Customer"), frappe.bold(self.party_name) + ), + title=_("Item Restricted for Party"), + ) + def validate_qty(self): for item in self.items: if flt(item.qty) <= 0: diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py index 136bc8fbb20..734951e97d4 100644 --- a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py @@ -94,3 +94,22 @@ class TestPartySpecificItem(FrappeTestCase): standalone_note = frappe.copy_doc(credit_note) standalone_note.return_against = None self.assertRaisesRegex(frappe.ValidationError, "is not allowed for", standalone_note.insert) + + def test_opportunity_enforces_party_rules(self): + from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity + + create_party_specific_item( + party_type="Customer", + party="_Test Customer 1", + restrict_based_on="Item", + based_on_value="_Test Item Home Desktop 100", + ) + make_opportunity(with_items=1, customer="_Test Customer 1", item_code="_Test Item Home Desktop 100") + self.assertRaisesRegex( + frappe.ValidationError, + "is not allowed for Customer", + make_opportunity, + with_items=1, + customer="_Test Customer 1", + item_code="_Test Item", + )