From 4671d1a6658107df592f88f8eb7485847e3d7ae2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 10 Sep 2026 20:51:48 +0530 Subject: [PATCH] fix(stock): apply company restriction to Item Price (#58948) --- erpnext/hooks.py | 2 + .../company_restriction.py | 32 +++++++++ .../test_company_restriction.py | 65 ++++++++++++++++--- 3 files changed, 90 insertions(+), 9 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 7f353808fbb..ddd82f29701 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -330,12 +330,14 @@ permission_query_conditions = { "Item": "erpnext.stock.doctype.company_restriction.company_restriction.get_permission_query_conditions", "Customer": "erpnext.stock.doctype.company_restriction.company_restriction.get_permission_query_conditions", "Supplier": "erpnext.stock.doctype.company_restriction.company_restriction.get_permission_query_conditions", + "Item Price": "erpnext.stock.doctype.company_restriction.company_restriction.get_inherited_permission_query_conditions", } has_permission = { "Item": "erpnext.stock.doctype.company_restriction.company_restriction.has_permission", "Customer": "erpnext.stock.doctype.company_restriction.company_restriction.has_permission", "Supplier": "erpnext.stock.doctype.company_restriction.company_restriction.has_permission", + "Item Price": "erpnext.stock.doctype.company_restriction.company_restriction.has_inherited_permission", } has_website_permission = { diff --git a/erpnext/stock/doctype/company_restriction/company_restriction.py b/erpnext/stock/doctype/company_restriction/company_restriction.py index f43315f6833..6ca14a8741a 100644 --- a/erpnext/stock/doctype/company_restriction/company_restriction.py +++ b/erpnext/stock/doctype/company_restriction/company_restriction.py @@ -11,6 +11,8 @@ from pypika.terms import Bracket, ExistsCriterion RESTRICTABLE_MASTER_DOCTYPES = ("Item", "Customer", "Supplier") +RESTRICTION_INHERITED_FROM = {"Item Price": ("Item", "item_code")} + COMPANY_RESTRICTION_EXEMPT_DOCTYPES = frozenset( { "Asset", @@ -71,6 +73,25 @@ def get_permission_query_conditions(user, doctype=None): return get_restriction_criterion(doctype, allowed_companies) +def get_inherited_permission_query_conditions(user, doctype=None): + if not (inherited := RESTRICTION_INHERITED_FROM.get(doctype)): + return None + + master_doctype, fieldname = inherited + allowed_companies = get_allowed_companies(user, master_doctype) + if not allowed_companies: + return None + + child = frappe.qb.DocType(doctype) + master = frappe.qb.DocType(master_doctype) + allowed_masters = ( + frappe.qb.from_(master) + .select(master.name) + .where(get_restriction_criterion(master_doctype, allowed_companies)) + ) + return child[fieldname].isin(allowed_masters) + + def get_restriction_criterion(doctype, companies): parent = frappe.qb.DocType(doctype) restriction = frappe.qb.DocType("Company Restriction") @@ -98,6 +119,17 @@ def has_permission(doc, ptype=None, user=None): return any(row.company in allowed_companies for row in doc.get("allowed_companies") or []) +def has_inherited_permission(doc, ptype=None, user=None): + if not (inherited := RESTRICTION_INHERITED_FROM.get(doc.doctype)): + return True + + master_doctype, fieldname = inherited + if not (master_name := doc.get(fieldname)): + return True + + return has_permission(frappe.get_cached_doc(master_doctype, master_name), ptype, user) + + def validate_allowed_companies(doc, method=None): if not doc.get("restrict_to_companies"): doc.set("allowed_companies", []) diff --git a/erpnext/stock/doctype/company_restriction/test_company_restriction.py b/erpnext/stock/doctype/company_restriction/test_company_restriction.py index 4b8f0c81b8e..1f8d9f7a05f 100644 --- a/erpnext/stock/doctype/company_restriction/test_company_restriction.py +++ b/erpnext/stock/doctype/company_restriction/test_company_restriction.py @@ -98,15 +98,7 @@ class TestCompanyRestriction(ERPNextTestSuite): def test_unrestricted_party_ignores_company_permission(self): customer = make_customer("_Test Party Details Company Permission Customer") user = self.make_user_with_roles("test_party_details_company@example.com", ["Sales User"]) - permission = { - "user": user, - "allow": "Company", - "for_value": "_Test Company 1", - "apply_to_all_doctypes": 1, - } - if not frappe.db.exists("User Permission", permission): - frappe.get_doc({"doctype": "User Permission", **permission}).insert(ignore_permissions=True) - frappe.clear_cache(user=user) + self.allow_company(user, "_Test Company 1") with self.set_user(user): results = party_query( @@ -155,6 +147,61 @@ class TestCompanyRestriction(ERPNextTestSuite): stock_entry.reload() stock_entry.cancel() + def allow_company(self, user, company): + permission = { + "user": user, + "allow": "Company", + "for_value": company, + "apply_to_all_doctypes": 1, + } + if not frappe.db.exists("User Permission", permission): + frappe.get_doc({"doctype": "User Permission", **permission}).insert(ignore_permissions=True) + frappe.clear_cache(user=user) + + def make_item_price(self, item_code): + return ( + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": "_Test Price List", + "item_code": item_code, + "price_list_rate": 100, + } + ) + .insert() + .name + ) + + def test_item_price_inherits_item_company_restriction(self): + restricted = make_item() + allowed = make_item() + self.restrict_to_companies("Item", restricted.name, ["_Test Company 1"]) + prices = {item.name: self.make_item_price(item.name) for item in (restricted, allowed)} + + user = self.make_user_with_roles("test_item_price_restriction@example.com", ["Sales Master Manager"]) + self.allow_company(user, "_Test Company") + + with self.set_user(user): + visible = frappe.get_list( + "Item Price", + filters={"item_code": ("in", [restricted.name, allowed.name])}, + pluck="item_code", + ) + self.assertEqual(visible, [allowed.name]) + + self.assertFalse(frappe.has_permission("Item Price", doc=prices[restricted.name])) + self.assertTrue(frappe.has_permission("Item Price", doc=prices[allowed.name])) + + def test_item_price_is_visible_without_company_permission(self): + restricted = make_item() + self.restrict_to_companies("Item", restricted.name, ["_Test Company 1"]) + price = self.make_item_price(restricted.name) + + user = self.make_user_with_roles("test_item_price_unrestricted@example.com", ["Sales Master Manager"]) + + with self.set_user(user): + self.assertTrue(frappe.has_permission("Item Price", doc=price)) + def make_user_with_roles(self, email, roles): if not frappe.db.exists("User", email): frappe.get_doc(