From ea5c648ab04a2b30c5c238f6cb299c4237ff1c1e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 18 Jul 2026 19:01:07 +0530 Subject: [PATCH] refactor: gate company restrictions per master via Restrict to Companies checkbox Replaces the Global Defaults toggle. Each Item/Customer/Supplier now carries a Restrict to Companies checkbox: the Allowed Companies table only shows (and is mandatory) when checked, is cleared on uncheck, and permission filtering, read denial and write validation apply only to masters that have the checkbox set. --- erpnext/buying/doctype/supplier/supplier.js | 6 +++++ erpnext/buying/doctype/supplier/supplier.json | 17 +++++++++---- erpnext/buying/doctype/supplier/supplier.py | 1 + erpnext/selling/doctype/customer/customer.js | 6 +++++ .../selling/doctype/customer/customer.json | 17 +++++++++---- erpnext/selling/doctype/customer/customer.py | 1 + .../global_defaults/global_defaults.json | 12 ++------- .../global_defaults/global_defaults.py | 2 -- .../company_restriction.py | 25 +++++++++++-------- erpnext/stock/doctype/item/item.js | 6 +++++ erpnext/stock/doctype/item/item.json | 17 +++++++++---- erpnext/stock/doctype/item/item.py | 1 + 12 files changed, 74 insertions(+), 37 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index acdbed969e8..c95bdf864e7 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -2,6 +2,12 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Supplier", { + restrict_to_companies(frm) { + if (!frm.doc.restrict_to_companies) { + frm.set_value("allowed_companies", []); + } + }, + setup: function (frm) { frm.set_query("allowed_companies", () => ({ query: "erpnext.stock.doctype.company_restriction.company_restriction.company_query", diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index caee355c57c..d9d8c11edb2 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -55,6 +55,7 @@ "tax_withholding_group", "settings_tab", "company_restrictions_section", + "restrict_to_companies", "allowed_companies", "invoice_settings_section", "is_transporter", @@ -430,16 +431,22 @@ { "fieldname": "company_restrictions_section", "fieldtype": "Section Break", - "label": "Company Restrictions", - "description": "If set, this Supplier is only available for transactions in the listed companies. Leave empty for no restriction.", - "depends_on": "eval:cint(frappe.sys_defaults.enable_company_wise_masters)" + "label": "Company Restrictions" + }, + { + "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." }, { "fieldname": "allowed_companies", "fieldtype": "Table MultiSelect", "label": "Allowed Companies", "options": "Company Restriction", - "depends_on": "eval:cint(frappe.sys_defaults.enable_company_wise_masters)" + "depends_on": "eval:doc.restrict_to_companies", + "mandatory_depends_on": "eval:doc.restrict_to_companies" }, { "fieldname": "contact_and_address_tab", @@ -578,7 +585,7 @@ "link_fieldname": "party" } ], - "modified": "2026-07-14 21:00:00.000000", + "modified": "2026-07-14 23:00:00.000000", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index e666c32b1c3..e36b9c05546 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -73,6 +73,7 @@ class Supplier(TransactionBase): primary_address: DF.TextEditor | None release_date: DF.Date | None represents_company: DF.Link | None + restrict_to_companies: DF.Check supplier_details: DF.Text | None supplier_group: DF.Link | None supplier_name: DF.Data diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 5ee6dd871c2..ae5d230bebd 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -2,6 +2,12 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Customer", { + restrict_to_companies(frm) { + if (!frm.doc.restrict_to_companies) { + frm.set_value("allowed_companies", []); + } + }, + setup: function (frm) { frm.set_query("allowed_companies", () => ({ query: "erpnext.stock.doctype.company_restriction.company_restriction.company_query", diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 786c834a06f..c6502200ac3 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -66,6 +66,7 @@ "tax_withholding_category", "settings_tab", "company_restrictions_section", + "restrict_to_companies", "allowed_companies", "section_break_ario", "so_required", @@ -516,18 +517,24 @@ "label": "Settings" }, { - "description": "If set, this Customer is only available for transactions in the listed companies. Leave empty for no restriction.", "fieldname": "company_restrictions_section", "fieldtype": "Section Break", - "label": "Company Restrictions", - "depends_on": "eval:cint(frappe.sys_defaults.enable_company_wise_masters)" + "label": "Company Restrictions" + }, + { + "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." }, { "fieldname": "allowed_companies", "fieldtype": "Table MultiSelect", "label": "Allowed Companies", "options": "Company Restriction", - "depends_on": "eval:cint(frappe.sys_defaults.enable_company_wise_masters)" + "depends_on": "eval:doc.restrict_to_companies", + "mandatory_depends_on": "eval:doc.restrict_to_companies" }, { "collapsible": 1, @@ -717,7 +724,7 @@ "link_fieldname": "party" } ], - "modified": "2026-07-14 21:00:00.000000", + "modified": "2026-07-14 23:00:00.000000", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 1bdae5b2944..1c150bb4676 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -99,6 +99,7 @@ class Customer(TransactionBase): primary_address: DF.TextEditor | None prospect_name: DF.Link | None represents_company: DF.Link | None + restrict_to_companies: DF.Check sales_team: DF.Table[SalesTeam] so_required: DF.Check supplier_numbers: DF.Table[SupplierNumberAtCustomer] diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.json b/erpnext/setup/doctype/global_defaults/global_defaults.json index 305972a5cea..8dc35200946 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.json +++ b/erpnext/setup/doctype/global_defaults/global_defaults.json @@ -17,8 +17,7 @@ "disable_rounded_total", "disable_in_words", "column_break_hnew", - "use_posting_datetime_for_naming_documents", - "enable_company_wise_masters" + "use_posting_datetime_for_naming_documents" ], "fields": [ { @@ -93,13 +92,6 @@ "fieldtype": "Check", "label": "Use Posting Datetime for Naming Documents" }, - { - "default": "0", - "description": "When enabled, Supplier, Customer, and Item records can be restricted to specific companies via their Allowed Companies table. Transactions will only show masters configured for the selected company.", - "fieldname": "enable_company_wise_masters", - "fieldtype": "Check", - "label": "Enable Company-wise Master Filtering" - }, { "fieldname": "defaults_section", "fieldtype": "Section Break", @@ -121,7 +113,7 @@ "in_create": 1, "issingle": 1, "links": [], - "modified": "2026-07-14 18:30:00.000000", + "modified": "2026-07-14 23:00:00.000000", "modified_by": "Administrator", "module": "Setup", "name": "Global Defaults", diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py index 9684566d3a9..8930390e4b3 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.py +++ b/erpnext/setup/doctype/global_defaults/global_defaults.py @@ -18,7 +18,6 @@ keydict = { "account_url": "account_url", "disable_rounded_total": "disable_rounded_total", "disable_in_words": "disable_in_words", - "enable_company_wise_masters": "enable_company_wise_masters", } ROUNDED_TOTAL_DOCTYPES = ( @@ -52,7 +51,6 @@ class GlobalDefaults(Document): demo_company: DF.Link | None disable_in_words: DF.Check disable_rounded_total: DF.Check - enable_company_wise_masters: DF.Check hide_currency_symbol: DF.Check use_posting_datetime_for_naming_documents: DF.Check # end: auto-generated types diff --git a/erpnext/stock/doctype/company_restriction/company_restriction.py b/erpnext/stock/doctype/company_restriction/company_restriction.py index 6b995e75a33..2a4aee36fb2 100644 --- a/erpnext/stock/doctype/company_restriction/company_restriction.py +++ b/erpnext/stock/doctype/company_restriction/company_restriction.py @@ -26,9 +26,6 @@ class CompanyRestriction(Document): def get_allowed_companies(user, doctype): from frappe.permissions import get_allowed_docs_for_doctype, get_user_permissions - if not frappe.get_single_value("Global Defaults", "enable_company_wise_masters"): - return None - user_permissions = get_user_permissions(user or frappe.session.user) if "Company" not in user_permissions: return None @@ -45,31 +42,39 @@ def get_permission_query_conditions(user, doctype=None): parent = frappe.qb.DocType(doctype) restriction = frappe.qb.DocType("Company Restriction") - restriction_rows = ( + allowed_rows = ( frappe.qb.from_(restriction) .select(restriction.name) .where( (restriction.parenttype == doctype) & (restriction.parentfield == "allowed_companies") & (restriction.parent == parent.name) + & (restriction.company.isin(allowed_companies)) ) ) - allowed_rows = restriction_rows.where(restriction.company.isin(allowed_companies)) - return Bracket(ExistsCriterion(allowed_rows) | ExistsCriterion(restriction_rows).negate()) + return Bracket((parent.restrict_to_companies == 0) | ExistsCriterion(allowed_rows)) def has_permission(doc, ptype=None, user=None): + if not doc.get("restrict_to_companies"): + return True + allowed_companies = get_allowed_companies(user, doc.doctype) if not allowed_companies: return True - companies = [row.company for row in doc.get("allowed_companies") or []] - if not companies: - return True - return any(company in allowed_companies for company in companies) + return any(row.company in allowed_companies for row in doc.get("allowed_companies") or []) def validate_allowed_companies(doc): + if not doc.get("restrict_to_companies"): + doc.set("allowed_companies", []) + elif not doc.get("allowed_companies") and not doc.flags.ignore_mandatory: + frappe.throw( + _("Allowed Companies is required when Restrict to Companies is checked"), + frappe.MandatoryError, + ) + if doc.flags.ignore_permissions: return diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index d59a208dad6..fa6be1fdd6f 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -60,6 +60,12 @@ frappe.ui.form.on("Item", { erpnext.utils.confirm_negative_stock(frm); }, + restrict_to_companies(frm) { + if (!frm.doc.restrict_to_companies) { + frm.set_value("allowed_companies", []); + } + }, + setup: function (frm) { frm.set_query("allowed_companies", () => ({ query: "erpnext.stock.doctype.company_restriction.company_restriction.company_query", diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 81975cd50f1..ea6f7023463 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -41,6 +41,7 @@ "column_break_wugd", "over_billing_allowance", "company_restrictions_section", + "restrict_to_companies", "allowed_companies", "section_break_11", "brand", @@ -1089,16 +1090,22 @@ { "fieldname": "company_restrictions_section", "fieldtype": "Section Break", - "label": "Company Restrictions", - "description": "If set, this Item is only available for transactions in the listed companies. Leave empty for no restriction.", - "depends_on": "eval:cint(frappe.sys_defaults.enable_company_wise_masters)" + "label": "Company Restrictions" + }, + { + "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." }, { "fieldname": "allowed_companies", "fieldtype": "Table MultiSelect", "label": "Allowed Companies", "options": "Company Restriction", - "depends_on": "eval:cint(frappe.sys_defaults.enable_company_wise_masters)" + "depends_on": "eval:doc.restrict_to_companies", + "mandatory_depends_on": "eval:doc.restrict_to_companies" } ], "icon": "fa fa-tag", @@ -1106,7 +1113,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2026-07-14 21:00:00.000000", + "modified": "2026-07-14 23:00:00.000000", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 1fc62169daa..e00fe9c8fd8 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -134,6 +134,7 @@ class Item(Document): quality_inspection_template: DF.Link | None reorder_levels: DF.Table[ItemReorder] retain_sample: DF.Check + restrict_to_companies: DF.Check safety_stock: DF.Float sales_tax_withholding_category: DF.Link | None sales_uom: DF.Link | None