From 8b09ba429e2c4779890fda1fe6543943fb124277 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Fri, 7 Aug 2026 17:20:00 +0530 Subject: [PATCH] fix: apply Sales Person user permissions in Accounts Receivable The report only narrowed by sales person when the filter was set, so a user restricted to a Sales Person saw every row once the filter was cleared. Resolve the permitted Sales Persons from user permissions and apply them on top of the filter. Each Sales Team parent type is matched against its own applicable_for scope, so a permission scoped to one doctype cannot authorise rows through the other. Descendants are already expanded by get_user_permissions, so Hide Descendants is respected. Gated to Receivable, since the class is shared with Accounts Payable. --- .../accounts_receivable.py | 63 ++++++++++++++----- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 89c617e25d9..6f5459de03e 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -6,6 +6,7 @@ from collections import OrderedDict import frappe from frappe import _, qb, query_builder, scrub +from frappe.permissions import get_allowed_docs_for_doctype from frappe.query_builder import Criterion from frappe.query_builder.functions import Date, Substring, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate @@ -51,6 +52,7 @@ class ReceivablePayableReport: self.filters = frappe._dict(filters or {}) self.qb_selection_filter = [] self.ple = qb.DocType("Payment Ledger Entry") + self.sales_person_records = None self.filters.report_date = getdate(self.filters.report_date or nowdate()) self.age_as_on = ( getdate(nowdate()) @@ -91,6 +93,7 @@ class ReceivablePayableReport: self.party_type = get_party_types_from_account_type(self.account_type) self.party_details = {} self.invoices = set() + self.sales_person_records = None self.skip_total_row = 0 self.advance_payment_doctypes = get_advance_payment_doctypes() @@ -205,7 +208,7 @@ class ReceivablePayableReport: def get_invoices(self, ple): if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"): - if self.filters.get("sales_person"): + if self.sales_person_records is not None: if ple.voucher_no in self.sales_person_records.get( "Sales Invoice", [] ) or ple.party in self.sales_person_records.get("Customer", []): @@ -236,7 +239,7 @@ class ReceivablePayableReport: ] def get_voucher_balance(self, ple): - if self.filters.get("sales_person"): + if self.sales_person_records is not None: if not ( ple.party in self.sales_person_records.get("Customer", []) or ple.against_voucher_no in self.sales_person_records.get("Sales Invoice", []) @@ -894,28 +897,54 @@ class ReceivablePayableReport: self.ple_query = query + def get_permitted_sales_persons(self, parenttype): + if self.account_type != "Receivable": + return None + + permissions = frappe.permissions.get_user_permissions(frappe.session.user).get("Sales Person", []) + if not permissions: + return None + + return get_allowed_docs_for_doctype(permissions, parenttype) + def get_sales_invoices_or_customers_based_on_sales_person(self): + parenttypes = ["Customer", "Sales Invoice"] + permitted = {p: self.get_permitted_sales_persons(p) for p in parenttypes} + + if not (self.filters.get("sales_person") or any(p is not None for p in permitted.values())): + return + + steam = frappe.qb.DocType("Sales Team") + + scope = [] + for parenttype in parenttypes: + criterion = steam.parenttype == parenttype + if (allowed := permitted[parenttype]) is not None: + criterion &= steam.sales_person.isin(allowed or [""]) + scope.append(criterion) + + conditions = [Criterion.any(scope)] + if self.filters.get("sales_person"): lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"]) - - steam = frappe.qb.DocType("Sales Team") sp = frappe.qb.DocType("Sales Person") - records = ( - frappe.qb.from_(steam) - .select(steam.parent, steam.parenttype) - .distinct() - .where( - steam.parenttype.isin(["Customer", "Sales Invoice"]) - & steam.sales_person.isin( - frappe.qb.from_(sp).select(sp.name).where((sp.lft >= lft) & (sp.rgt <= rgt)) - ) + conditions.append( + steam.sales_person.isin( + frappe.qb.from_(sp).select(sp.name).where((sp.lft >= lft) & (sp.rgt <= rgt)) ) - .run(as_dict=1) ) - self.sales_person_records = frappe._dict() - for d in records: - self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent) + records = ( + frappe.qb.from_(steam) + .select(steam.parent, steam.parenttype) + .distinct() + .where(Criterion.all(conditions)) + .run(as_dict=1) + ) + + self.sales_person_records = frappe._dict() + for d in records: + self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent) def get_invoices_based_on_sales_partner(self): if not self.filters.get("sales_partner"):