fix(accounts): filter parties by transaction company (#58147)

This commit is contained in:
Mihir Kandoi
2026-08-14 11:02:13 +05:30
committed by GitHub
parent 9239d1c2a3
commit 8632119f11
9 changed files with 183 additions and 14 deletions

View File

@@ -476,6 +476,8 @@ frappe.ui.form.on("Payment Entry", {
return {
query: "erpnext.controllers.queries.employee_query",
};
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
return erpnext.queries.party(frm.doc);
} else if (frm.doc.party_type == "Shareholder") {
return {
filters: {

View File

@@ -40,6 +40,7 @@ from erpnext.accounts.party import (
complete_contact_details,
get_default_contact,
get_party_account,
validate_party_company,
)
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
@@ -2434,6 +2435,7 @@ def get_party_details(company: str, party_type: str, party: str, date: str, cost
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
frappe.has_permission(party_type, ptype, party, throw=True)
validate_party_company(party_type, party, company)
party_account = get_party_account(party_type, party, company)
account_currency = get_account_currency(party_account)

View File

@@ -140,6 +140,7 @@ def _get_party_details(
if not ignore_permissions:
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
frappe.has_permission(party_type, ptype, party, throw=True)
validate_party_company(party_type, party.name, company)
currency = party.get("default_currency") or currency or get_company_currency(company)
@@ -197,6 +198,17 @@ def _get_party_details(
return party_details
def validate_party_company(party_type, party, company):
if not company or party_type not in ("Customer", "Supplier"):
return
from erpnext.stock.doctype.company_restriction.company_restriction import (
validate_masters_for_company,
)
validate_masters_for_company(party_type, [party], company)
def set_address_details(
party_details,
party,

View File

@@ -28,6 +28,7 @@ frappe.ui.form.on("Request for Quotation", {
is_group: 0,
},
}));
frm.set_query("supplier", "suppliers", () => erpnext.queries.supplier(frm.doc));
frm.set_indicator_formatter("item_code", function (doc) {
return !doc.qty && frm.doc.has_unit_price_items ? "yellow" : "";
@@ -339,6 +340,7 @@ frappe.ui.form.on("Request for Quotation Supplier", {
args: {
party: d.supplier,
party_type: "Supplier",
company: frm.doc.company,
},
callback: function (r) {
if (r.message) {

View File

@@ -6,7 +6,7 @@ import json
from collections import OrderedDict, defaultdict
import frappe
from frappe import qb, scrub
from frappe import _, qb, scrub
from frappe.permissions import has_permission
from frappe.query_builder import Case, Criterion, DocType
from frappe.query_builder.functions import (
@@ -212,6 +212,65 @@ def tax_account_query(doctype: str, txt: str, searchfield: str, start: int, page
return tax_accounts
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def party_query(
doctype: str,
txt: str,
searchfield: str,
start: int,
page_len: int,
filters: dict | str | None = None,
):
party_name_field = {"Customer": "customer_name", "Supplier": "supplier_name"}.get(doctype)
if not party_name_field:
frappe.throw(_("Invalid party type: {0}").format(doctype))
filters = frappe.parse_json(filters) if filters else {}
if not isinstance(filters, dict):
frappe.throw(_("Party query filters must be a dictionary"))
company = filters.pop("company", None)
fields = get_fields(doctype, ["name", party_name_field])
party = DocType(doctype)
search_str = f"%{txt}%"
txt_no_percent = txt.replace("%", "")
search_fields = list(dict.fromkeys([searchfield, *fields]))
search_conditions = [party[field].like(search_str) for field in search_fields]
query = (
frappe.qb.get_query(doctype, fields=fields, filters=filters, ignore_permissions=False)
.where(party.docstatus < 2)
.where(Criterion.any(search_conditions))
.orderby(
Case()
.when(
Locate(Lower(txt_no_percent), Lower(party.name)) > 0,
Locate(Lower(txt_no_percent), Lower(party.name)),
)
.else_(99999)
)
.orderby(
Case()
.when(
Locate(Lower(txt_no_percent), Lower(party[party_name_field])) > 0,
Locate(Lower(txt_no_percent), Lower(party[party_name_field])),
)
.else_(99999)
)
.orderby(party.idx, order=Order.desc)
.orderby(party.name)
.orderby(party[party_name_field])
.limit(page_len)
.offset(start)
)
if company:
query = query.where(get_restriction_criterion(doctype, [company]))
return query.run()
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_query(

View File

@@ -12,8 +12,19 @@ $.extend(erpnext.queries, {
return { query: "erpnext.controllers.queries.lead_query" };
},
customer: function () {
return { filters: { disabled: 0 } };
party: function (doc) {
return {
query: "erpnext.controllers.queries.party_query",
filters: { disabled: 0, company: doc.company },
};
},
customer: function (doc) {
return erpnext.queries.party(doc);
},
supplier: function (doc) {
return erpnext.queries.party(doc);
},
item: function (filters) {

View File

@@ -204,7 +204,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
set_dynamic_field_label() {
if (this.frm.doc.quotation_to == "Customer") {
this.frm.set_df_property("party_name", "label", "Customer");
this.frm.fields_dict.party_name.get_query = null;
this.frm.fields_dict.party_name.get_query = erpnext.queries.customer;
} else if (this.frm.doc.quotation_to == "Lead") {
this.frm.set_df_property("party_name", "label", "Lead");
this.frm.fields_dict.party_name.get_query = function () {

View File

@@ -140,16 +140,20 @@ def validate_transaction_company(doc, method=None):
return
for doctype, names in get_master_references(doc).items():
if blocked := get_blocked_masters(doctype, names, company):
frappe.throw(
_("{0} {1} cannot be used with Company {2} because of Company Restrictions").format(
_(doctype),
comma_and([frappe.bold(name) for name in blocked], add_quotes=False),
frappe.bold(company),
),
CompanyRestrictionError,
title=_("Restricted to Other Companies"),
)
validate_masters_for_company(doctype, names, company)
def validate_masters_for_company(doctype, names, company):
if blocked := get_blocked_masters(doctype, names, company):
frappe.throw(
_("{0} {1} cannot be used with Company {2} because of Company Restrictions").format(
_(doctype),
comma_and([frappe.bold(name) for name in blocked], add_quotes=False),
frappe.bold(company),
),
CompanyRestrictionError,
title=_("Restricted to Other Companies"),
)
def get_master_references(doc):

View File

@@ -3,8 +3,10 @@
import frappe
from erpnext.accounts.party import get_party_details
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.queries import party_query
from erpnext.selling.doctype.customer.test_customer import make_customer
from erpnext.selling.doctype.quotation.test_quotation import make_quotation
from erpnext.stock.doctype.company_restriction.company_restriction import CompanyRestrictionError
@@ -51,6 +53,81 @@ class TestCompanyRestriction(ERPNextTestSuite):
self.restrict_to_companies("Supplier", supplier.name, ["_Test Company"])
create_purchase_order(supplier=supplier.name, do_not_submit=1)
def test_party_query_filters_customer_and_supplier_by_transaction_company(self):
customer = make_customer("_Test Company Query Restricted Customer")
supplier = create_supplier(supplier_name="_Test Company Query Restricted Supplier")
for doctype, party in (("Customer", customer), ("Supplier", supplier.name)):
self.restrict_to_companies(doctype, party, ["_Test Company 1"])
results = party_query(
doctype,
party,
"name",
0,
20,
filters={"disabled": 0, "company": "_Test Company"},
)
self.assertNotIn(party, [row[0] for row in results])
results = party_query(
doctype,
party,
"name",
0,
20,
filters={"disabled": 0, "company": "_Test Company 1"},
)
self.assertIn(party, [row[0] for row in results])
def test_get_party_details_checks_transaction_company_restriction(self):
customer = make_customer("_Test Party Details Restricted Customer")
supplier = create_supplier(supplier_name="_Test Party Details Restricted Supplier")
for doctype, party in (("Customer", customer), ("Supplier", supplier.name)):
self.restrict_to_companies(doctype, party, ["_Test Company 1"])
self.assertRaises(
CompanyRestrictionError,
get_party_details,
party=party,
party_type=doctype,
company="_Test Company",
)
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)
frappe.set_user(user)
self.addCleanup(frappe.set_user, "Administrator")
results = party_query(
"Customer",
customer,
"name",
0,
20,
filters={"disabled": 0, "company": "_Test Company"},
)
self.assertIn(customer, [row[0] for row in results])
details = get_party_details(
party=customer,
party_type="Customer",
company="_Test Company",
)
self.assertEqual(details.customer, customer)
def test_unrestricted_item_is_not_blocked(self):
item = make_item()
make_material_request(item_code=item.name)