mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-15 15:38:39 +00:00
fix(accounts): filter parties by transaction company (#58147)
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user