From c03a66a1bf48a53c42d01c9d936d9b22aa013e11 Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:53:33 +0530 Subject: [PATCH 1/3] fix: apply user permissions to receivable/payable reports (cherry picked from commit b05abbc53b3655b02db17ba2e8165519f195c1c2) --- .../accounts_receivable.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index d28f886a4fd..e83311647b2 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -922,8 +922,28 @@ class ReceivablePayableReport: if self.filters.project: self.qb_selection_filter.append(self.ple.project.isin(self.filters.project)) + self.add_user_permission_filters() + self.add_accounting_dimensions_filters() + def add_user_permission_filters(self): + # Party is a dynamic link, so match conditions cannot auto-apply Customer/Supplier user permissions + from frappe.core.doctype.user_permission.user_permission import get_user_permissions + from frappe.permissions import get_allowed_docs_for_doctype + + user_permissions = get_user_permissions() + if not user_permissions: + return + + for party_type in self.party_type: + if party_type not in user_permissions: + continue + + allowed_parties = get_allowed_docs_for_doctype(user_permissions[party_type], party_type) + self.qb_selection_filter.append( + (self.ple.party_type != party_type) | self.ple.party.isin(allowed_parties or [""]) + ) + def get_cost_center_conditions(self): cost_center_list = get_cost_centers_with_children(self.filters.cost_center) self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list)) From b9a694bb37fb4f72167594580465c83cb6bb0f83 Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:58:21 +0530 Subject: [PATCH 2/3] test: cover user permission scoping in receivable report (cherry picked from commit 4200d17c9b208c72749e426a66a09cb48aeadbd9) --- .../test_accounts_receivable.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 93130fa353a..e48883660e6 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1253,3 +1253,44 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): self.assertEqual(len(report[1]), 1) row = report[1][0] self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding]) + + def test_accounts_receivable_respects_user_permissions(self): + # Party is a dynamic link on Payment Ledger Entry, so user permissions on Customer + # must be applied explicitly. The report should only show permitted customers. + original_customer = self.customer + second_customer = "_Test AR Perm Customer" + + # create_customer overrides self.customer, so build the restricted invoice first + self.create_customer(customer_name=second_customer) + self.create_sales_invoice(no_payment_schedule=True) + + self.customer = original_customer + allowed_invoice = self.create_sales_invoice(no_payment_schedule=True) + + test_user = "test_ar_user_permission@example.com" + if not frappe.db.exists("User", test_user): + user = frappe.new_doc("User") + user.email = test_user + user.first_name = "AR Perm" + user.append("roles", {"role": "Accounts User"}) + user.save() + + frappe.permissions.add_user_permission("Customer", original_customer, test_user) + + filters = { + "company": self.company, + "party_type": "Customer", + "report_date": today(), + "range": "30, 60, 90, 120", + } + + frappe.set_user(test_user) + try: + report = execute(filters) + finally: + frappe.set_user("Administrator") + + parties = {row.party for row in report[1]} + self.assertIn(original_customer, parties) + self.assertNotIn(second_customer, parties) + self.assertEqual(allowed_invoice.customer, original_customer) From 7639a3360e2eab2683c37e040a51d1292dd5e856 Mon Sep 17 00:00:00 2001 From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:04:15 +0530 Subject: [PATCH 3/3] test: clean up receivable entries to avoid cross-report leakage --- .../accounts_receivable/test_accounts_receivable.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index e48883660e6..4a73d62ee2e 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1257,6 +1257,15 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): def test_accounts_receivable_respects_user_permissions(self): # Party is a dynamic link on Payment Ledger Entry, so user permissions on Customer # must be applied explicitly. The report should only show permitted customers. + + # Running the report writes an access log that commits, so these invoices survive + # tearDown's rollback. Delete and commit them so they don't leak into other tests. + def remove_committed_entries(): + self.clear_old_entries() + frappe.db.commit() # nosemgrep + + self.addCleanup(remove_committed_entries) + original_customer = self.customer second_customer = "_Test AR Perm Customer"