fix(stock): apply company restriction to Item Price (#58948)

This commit is contained in:
Mihir Kandoi
2026-09-10 20:51:48 +05:30
committed by GitHub
parent 5f216c5d55
commit 4671d1a665
3 changed files with 90 additions and 9 deletions

View File

@@ -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 = {

View File

@@ -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", [])

View File

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