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.
This commit is contained in:
Mihir Kandoi
2026-07-18 19:01:07 +05:30
parent 4d49cfa1de
commit ea5c648ab0
12 changed files with 74 additions and 37 deletions

View File

@@ -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",

View File

@@ -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",

View File

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

View File

@@ -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",

View File

@@ -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",

View File

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

View File

@@ -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 <b>Allowed Companies</b> 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",

View File

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

View File

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

View File

@@ -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",

View File

@@ -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",

View File

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