From 565220ebe5931825f4a9d1b42e314322911b9bf7 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 23 Jul 2026 12:07:16 +0530 Subject: [PATCH 1/3] fix: gate company restriction fields behind permlevel 1 Only the master-manager role of each doctype (Item Manager, Sales Master Manager, Purchase Master Manager) can view and edit restrict_to_companies and allowed_companies. Customer reuses its existing level-1 permission rows; Item and Supplier get a new level-1 row. --- erpnext/buying/doctype/supplier/supplier.json | 17 +++++++++--- .../selling/doctype/customer/customer.json | 11 +++++--- .../test_company_restriction.py | 27 +++++++++++++++++++ erpnext/stock/doctype/item/item.json | 17 +++++++++--- 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index d9d8c11edb2..20cedab2afd 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -431,14 +431,16 @@ { "fieldname": "company_restrictions_section", "fieldtype": "Section Break", - "label": "Company Restrictions" + "label": "Company Restrictions", + "permlevel": 1 }, { "default": "0", "fieldname": "restrict_to_companies", "fieldtype": "Check", "label": "Restrict to Companies", - "description": "If checked, this Supplier is only available for transactions in the companies listed below." + "description": "If checked, this Supplier is only available for transactions in the companies listed below.", + "permlevel": 1 }, { "fieldname": "allowed_companies", @@ -446,7 +448,8 @@ "label": "Allowed Companies", "options": "Company Restriction", "depends_on": "eval:doc.restrict_to_companies", - "mandatory_depends_on": "eval:doc.restrict_to_companies" + "mandatory_depends_on": "eval:doc.restrict_to_companies", + "permlevel": 1 }, { "fieldname": "contact_and_address_tab", @@ -585,7 +588,7 @@ "link_fieldname": "party" } ], - "modified": "2026-07-14 23:00:00.000000", + "modified": "2026-07-23 10:00:00.000000", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", @@ -641,6 +644,12 @@ "read": 1, "report": 1, "role": "Accounts Manager" + }, + { + "permlevel": 1, + "read": 1, + "role": "Purchase Master Manager", + "write": 1 } ], "quick_entry": 1, diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index f14a0d223e8..a37a94cb850 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -519,14 +519,16 @@ { "fieldname": "company_restrictions_section", "fieldtype": "Section Break", - "label": "Company Restrictions" + "label": "Company Restrictions", + "permlevel": 1 }, { "default": "0", "fieldname": "restrict_to_companies", "fieldtype": "Check", "label": "Restrict to Companies", - "description": "If checked, this Customer is only available for transactions in the companies listed below." + "description": "If checked, this Customer is only available for transactions in the companies listed below.", + "permlevel": 1 }, { "fieldname": "allowed_companies", @@ -534,7 +536,8 @@ "label": "Allowed Companies", "options": "Company Restriction", "depends_on": "eval:doc.restrict_to_companies", - "mandatory_depends_on": "eval:doc.restrict_to_companies" + "mandatory_depends_on": "eval:doc.restrict_to_companies", + "permlevel": 1 }, { "collapsible": 1, @@ -724,7 +727,7 @@ "link_fieldname": "party" } ], - "modified": "2026-07-14 23:00:00.000000", + "modified": "2026-07-23 10:00:00.000000", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/stock/doctype/company_restriction/test_company_restriction.py b/erpnext/stock/doctype/company_restriction/test_company_restriction.py index 28b1680c7c1..6bfed075be9 100644 --- a/erpnext/stock/doctype/company_restriction/test_company_restriction.py +++ b/erpnext/stock/doctype/company_restriction/test_company_restriction.py @@ -79,3 +79,30 @@ class TestCompanyRestriction(ERPNextTestSuite): self.restrict_to_companies("Item", item.name, ["_Test Company 1"]) stock_entry.reload() stock_entry.cancel() + + def test_restriction_fields_require_permlevel_access(self): + customer = make_customer("_Test Permlevel Restricted Customer") + self.restrict_to_companies("Customer", customer, ["_Test Company"]) + + user = "test_company_restriction_perm@example.com" + if not frappe.db.exists("User", user): + frappe.get_doc( + { + "doctype": "User", + "email": user, + "first_name": "Company Restriction Perm", + "roles": [{"role": "Sales User"}], + } + ).insert(ignore_permissions=True) + + frappe.set_user(user) + self.addCleanup(frappe.set_user, "Administrator") + + doc = frappe.get_doc("Customer", customer) + doc.restrict_to_companies = 0 + doc.set("allowed_companies", []) + doc.save() + + doc.reload() + self.assertEqual(doc.restrict_to_companies, 1) + self.assertEqual([row.company for row in doc.allowed_companies], ["_Test Company"]) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index ea6f7023463..4594da25d6c 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -1090,14 +1090,16 @@ { "fieldname": "company_restrictions_section", "fieldtype": "Section Break", - "label": "Company Restrictions" + "label": "Company Restrictions", + "permlevel": 1 }, { "default": "0", "fieldname": "restrict_to_companies", "fieldtype": "Check", "label": "Restrict to Companies", - "description": "If checked, this Item is only available for transactions in the companies listed below." + "description": "If checked, this Item is only available for transactions in the companies listed below.", + "permlevel": 1 }, { "fieldname": "allowed_companies", @@ -1105,7 +1107,8 @@ "label": "Allowed Companies", "options": "Company Restriction", "depends_on": "eval:doc.restrict_to_companies", - "mandatory_depends_on": "eval:doc.restrict_to_companies" + "mandatory_depends_on": "eval:doc.restrict_to_companies", + "permlevel": 1 } ], "icon": "fa fa-tag", @@ -1113,7 +1116,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2026-07-14 23:00:00.000000", + "modified": "2026-07-23 10:00:00.000000", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1175,6 +1178,12 @@ "role": "Desk User", "select": 1, "share": 1 + }, + { + "permlevel": 1, + "read": 1, + "role": "Item Manager", + "write": 1 } ], "quick_entry": 1, From 41fc23b9b26f18000fd1c56f9be3e28739a2f4cc Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 23 Jul 2026 12:12:49 +0530 Subject: [PATCH 2/3] fix: drop Customer's unused level-1 Sales User read grant Makes visibility uniform across the three masters: only the master-manager role can see or edit company restriction fields. --- .../selling/doctype/customer/customer.json | 7 +--- .../test_company_restriction.py | 36 +++++++++++++------ 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index a37a94cb850..24aeebf7544 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -727,7 +727,7 @@ "link_fieldname": "party" } ], - "modified": "2026-07-23 10:00:00.000000", + "modified": "2026-07-23 12:00:00.000000", "modified_by": "Administrator", "module": "Selling", "name": "Customer", @@ -744,11 +744,6 @@ "share": 1, "write": 1 }, - { - "permlevel": 1, - "read": 1, - "role": "Sales User" - }, { "email": 1, "print": 1, diff --git a/erpnext/stock/doctype/company_restriction/test_company_restriction.py b/erpnext/stock/doctype/company_restriction/test_company_restriction.py index 6bfed075be9..719860c1f0b 100644 --- a/erpnext/stock/doctype/company_restriction/test_company_restriction.py +++ b/erpnext/stock/doctype/company_restriction/test_company_restriction.py @@ -80,22 +80,36 @@ class TestCompanyRestriction(ERPNextTestSuite): stock_entry.reload() stock_entry.cancel() + def make_user_with_roles(self, email, roles): + if not frappe.db.exists("User", email): + frappe.get_doc( + { + "doctype": "User", + "email": email, + "first_name": email.split("@")[0], + "roles": [{"role": role} for role in roles], + } + ).insert(ignore_permissions=True) + return email + def test_restriction_fields_require_permlevel_access(self): customer = make_customer("_Test Permlevel Restricted Customer") self.restrict_to_companies("Customer", customer, ["_Test Company"]) - user = "test_company_restriction_perm@example.com" - if not frappe.db.exists("User", user): - frappe.get_doc( - { - "doctype": "User", - "email": user, - "first_name": "Company Restriction Perm", - "roles": [{"role": "Sales User"}], - } - ).insert(ignore_permissions=True) + sales_user = self.make_user_with_roles("test_company_restriction_sales@example.com", ["Sales User"]) + manager = self.make_user_with_roles( + "test_company_restriction_manager@example.com", ["Sales User", "Sales Master Manager"] + ) - frappe.set_user(user) + permitted = frappe.get_meta("Customer").get_permitted_fieldnames(user=sales_user) + self.assertNotIn("restrict_to_companies", permitted) + self.assertNotIn("allowed_companies", permitted) + + permitted = frappe.get_meta("Customer").get_permitted_fieldnames(user=manager) + self.assertIn("restrict_to_companies", permitted) + self.assertIn("allowed_companies", permitted) + + frappe.set_user(sales_user) self.addCleanup(frappe.set_user, "Administrator") doc = frappe.get_doc("Customer", customer) From 4bb63cca105f2e9fcaffcd07bdfd7be98fb7f11a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 23 Jul 2026 12:26:59 +0530 Subject: [PATCH 3/3] test: probe permlevel visibility via a value field only get_permitted_fieldnames never lists Table fields, so allowed_companies cannot be asserted through it; the write-reset assertions already cover that field. --- .../doctype/company_restriction/test_company_restriction.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/doctype/company_restriction/test_company_restriction.py b/erpnext/stock/doctype/company_restriction/test_company_restriction.py index 719860c1f0b..52c7da50101 100644 --- a/erpnext/stock/doctype/company_restriction/test_company_restriction.py +++ b/erpnext/stock/doctype/company_restriction/test_company_restriction.py @@ -103,11 +103,9 @@ class TestCompanyRestriction(ERPNextTestSuite): permitted = frappe.get_meta("Customer").get_permitted_fieldnames(user=sales_user) self.assertNotIn("restrict_to_companies", permitted) - self.assertNotIn("allowed_companies", permitted) permitted = frappe.get_meta("Customer").get_permitted_fieldnames(user=manager) self.assertIn("restrict_to_companies", permitted) - self.assertIn("allowed_companies", permitted) frappe.set_user(sales_user) self.addCleanup(frappe.set_user, "Administrator")