mirror of
https://github.com/frappe/erpnext.git
synced 2026-10-29 22:57:42 +00:00
fix(accounts): apply user permissions in party balance reports
(cherry picked from commit f0c96eb914)
# Conflicts:
# erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py
# erpnext/accounts/report/trial_balance_for_party/test_trial_balance_for_party.py
# erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py
This commit is contained in:
@@ -14,6 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_company_permission
|
||||
|
||||
TREE_DOCTYPES = frozenset(
|
||||
["Customer Group", "Territory", "Supplier Group", "Sales Partner", "Sales Person", "Cost Center"]
|
||||
@@ -49,6 +50,8 @@ class PartyLedgerSummaryReport:
|
||||
if not self.filters.get("company"):
|
||||
frappe.throw(_("{0} is mandatory").format(_("Company")))
|
||||
|
||||
validate_company_permission(self.filters.company)
|
||||
|
||||
if self.filters.from_date > self.filters.to_date:
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
|
||||
@@ -378,6 +381,11 @@ class PartyLedgerSummaryReport:
|
||||
(gle[dimension.fieldname]).isin(self.filters.get(dimension.fieldname))
|
||||
)
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
if match_conditions := build_match_conditions("GL Entry"):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
return query
|
||||
|
||||
def get_return_invoices(self):
|
||||
|
||||
@@ -304,3 +304,88 @@ class TestCustomerLedgerSummary(FrappeTestCase, AccountsTestMixin):
|
||||
f"Field {field} does not match expected value. "
|
||||
f"Expected: {expected_value}, Got: {actual_value}",
|
||||
)
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
def test_several_group_cost_centers_include_all_their_children(self):
|
||||
for group in ("_Test CLS Group A", "_Test CLS Group B"):
|
||||
create_cost_center(cost_center_name=group, is_group=1)
|
||||
create_cost_center(cost_center_name=f"{group} Child", parent_cost_center=f"{group} - _TC")
|
||||
self.cost_center = f"{group} Child - _TC"
|
||||
self.create_sales_invoice()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"cost_center": ["_Test CLS Group A - _TC", "_Test CLS Group B - _TC"],
|
||||
}
|
||||
|
||||
report = execute(filters)[1]
|
||||
|
||||
self.assertEqual([(row.party, row.invoiced_amount) for row in report], [(self.customer, 2000.0)])
|
||||
|
||||
def test_refund_of_credit_note_from_before_the_period_is_a_negative_payment(self):
|
||||
earlier = add_days(today(), -10)
|
||||
si = self.create_sales_invoice(do_not_submit=True)
|
||||
si.update({"posting_date": earlier, "set_posting_time": 1})
|
||||
si.save().submit()
|
||||
credit_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
credit_note.update({"posting_date": earlier, "set_posting_time": 1})
|
||||
credit_note.items[0].qty = -3
|
||||
credit_note.save().submit()
|
||||
get_payment_entry("Sales Invoice", credit_note.name, bank_account=self.cash).insert().submit()
|
||||
|
||||
row = execute({"company": self.company, "from_date": today(), "to_date": today()})[1][0]
|
||||
|
||||
self.assertEqual(
|
||||
(row.opening_balance, row.invoiced_amount, row.paid_amount, row.closing_balance),
|
||||
(700.0, 0.0, -300.0, 1000.0),
|
||||
)
|
||||
|
||||
def test_company_without_permission_is_refused(self):
|
||||
user = "test_cls_company_permission@example.com"
|
||||
if not frappe.db.exists("User", user):
|
||||
frappe.get_doc(
|
||||
{"doctype": "User", "email": user, "first_name": "CLS", "roles": [{"role": "Accounts User"}]}
|
||||
).insert()
|
||||
frappe.permissions.add_user_permission("Company", "_Test Company 1", user)
|
||||
|
||||
frappe.set_user(user)
|
||||
try:
|
||||
self.assertRaises(
|
||||
frappe.PermissionError,
|
||||
execute,
|
||||
{"company": self.company, "from_date": today(), "to_date": today()},
|
||||
)
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_amounts_follow_gl_entry_permissions(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
other_receivable = create_account(
|
||||
account_name="_Test CLS Receivable",
|
||||
company=self.company,
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
account_type="Receivable",
|
||||
)
|
||||
self.create_sales_invoice()
|
||||
permitted_receivable, self.debit_to = self.debit_to, other_receivable
|
||||
self.create_sales_invoice()
|
||||
|
||||
user = "test_cls_account_permission@example.com"
|
||||
if not frappe.db.exists("User", user):
|
||||
frappe.get_doc(
|
||||
{"doctype": "User", "email": user, "first_name": "CLS", "roles": [{"role": "Accounts User"}]}
|
||||
).insert()
|
||||
frappe.permissions.add_user_permission("Account", permitted_receivable, user)
|
||||
|
||||
frappe.set_user(user)
|
||||
try:
|
||||
report = execute({"company": self.company, "from_date": today(), "to_date": today()})[1]
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
self.assertEqual([(row.party, row.invoiced_amount) for row in report], [(self.customer, 1000.0)])
|
||||
>>>>>>> f0c96eb (fix(accounts): apply user permissions in party balance reports)
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.trial_balance_for_party.trial_balance_for_party import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestTrialBalanceForParty(ERPNextTestSuite):
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"party_type": "Customer",
|
||||
"fiscal_year": "_Test Fiscal Year 2026",
|
||||
"from_date": "2026-01-01",
|
||||
"to_date": "2026-12-31",
|
||||
**extra,
|
||||
}
|
||||
)
|
||||
return execute(filters)[1]
|
||||
|
||||
def party_row(self, party, **extra):
|
||||
return next(row for row in self.run_report(party=party, **extra) if row.get("party") == party)
|
||||
|
||||
def test_sales_invoice_shown_as_period_debit(self):
|
||||
customer = "_Test Customer"
|
||||
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2026-06-01")
|
||||
|
||||
row = self.party_row(customer)
|
||||
self.assertEqual(row["opening_debit"], 0)
|
||||
self.assertEqual(row["debit"], 10000)
|
||||
self.assertEqual(row["credit"], 0)
|
||||
self.assertEqual(row["closing_debit"], 10000)
|
||||
self.assertEqual(row["closing_credit"], 0)
|
||||
|
||||
def test_receipt_nets_invoice_in_closing(self):
|
||||
customer = "_Test Customer"
|
||||
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2026-06-01")
|
||||
create_payment_entry(
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=customer,
|
||||
paid_from="Debtors - _TC",
|
||||
paid_to="_Test Bank - _TC",
|
||||
paid_amount=4000,
|
||||
save=True,
|
||||
submit=True,
|
||||
)
|
||||
|
||||
row = self.party_row(customer)
|
||||
self.assertEqual(row["debit"], 10000)
|
||||
self.assertEqual(row["credit"], 4000)
|
||||
# closing nets debit against credit: 10000 - 4000
|
||||
self.assertEqual(row["closing_debit"], 6000)
|
||||
self.assertEqual(row["closing_credit"], 0)
|
||||
|
||||
def test_prior_period_invoice_shown_as_opening(self):
|
||||
customer = "_Test Customer"
|
||||
# invoice dated before from_date should land in the opening balance, not within-period
|
||||
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2025-12-01")
|
||||
|
||||
row = self.party_row(customer)
|
||||
self.assertEqual(row["opening_debit"], 10000)
|
||||
self.assertEqual(row["debit"], 0)
|
||||
self.assertEqual(row["closing_debit"], 10000)
|
||||
|
||||
def test_exclude_zero_balance_parties(self):
|
||||
customer = "_Test Customer"
|
||||
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2026-06-01")
|
||||
create_payment_entry(
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=customer,
|
||||
paid_from="Debtors - _TC",
|
||||
paid_to="_Test Bank - _TC",
|
||||
paid_amount=10000,
|
||||
save=True,
|
||||
submit=True,
|
||||
)
|
||||
|
||||
# fully settled party still shows by default ...
|
||||
self.assertEqual(self.party_row(customer)["closing_debit"], 0)
|
||||
# ... but is hidden when zero-balance parties are excluded
|
||||
parties = {row.get("party") for row in self.run_report(exclude_zero_balance_parties=1)}
|
||||
self.assertNotIn(customer, parties)
|
||||
|
||||
def test_purchase_invoice_shown_as_supplier_credit(self):
|
||||
supplier = "_Test Supplier"
|
||||
make_purchase_invoice(supplier=supplier, qty=1, rate=8000, posting_date="2026-06-01")
|
||||
|
||||
row = self.party_row(supplier, party_type="Supplier")
|
||||
self.assertEqual(row["credit"], 8000)
|
||||
self.assertEqual(row["debit"], 0)
|
||||
self.assertEqual(row["closing_credit"], 8000)
|
||||
self.assertEqual(row["closing_debit"], 0)
|
||||
|
||||
def test_totals_row_sums_party_rows(self):
|
||||
create_sales_invoice(customer="_Test Customer 1", qty=1, rate=10000, posting_date="2026-06-01")
|
||||
create_sales_invoice(customer="_Test Customer 2", qty=1, rate=6000, posting_date="2026-06-01")
|
||||
|
||||
data = self.run_report()
|
||||
totals = data[-1] # totals row is appended last
|
||||
party_rows = data[:-1]
|
||||
for column in (
|
||||
"opening_debit",
|
||||
"opening_credit",
|
||||
"debit",
|
||||
"credit",
|
||||
"closing_debit",
|
||||
"closing_credit",
|
||||
):
|
||||
self.assertEqual(totals[column], sum(row[column] for row in party_rows))
|
||||
|
||||
def test_totals_cover_only_permitted_parties(self):
|
||||
create_sales_invoice(customer="_Test Customer 1", qty=1, rate=10000, posting_date="2026-06-01")
|
||||
create_sales_invoice(customer="_Test Customer 2", qty=1, rate=6000, posting_date="2026-06-01")
|
||||
|
||||
user = "test_tbp_party_permission@example.com"
|
||||
if not frappe.db.exists("User", user):
|
||||
frappe.get_doc(
|
||||
{"doctype": "User", "email": user, "first_name": "TBP", "roles": [{"role": "Accounts User"}]}
|
||||
).insert()
|
||||
frappe.permissions.add_user_permission("Customer", "_Test Customer 1", user)
|
||||
|
||||
frappe.set_user(user)
|
||||
try:
|
||||
data = self.run_report()
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
self.assertEqual({row["party"] for row in data[:-1]}, {"_Test Customer 1"})
|
||||
self.assertEqual(data[-1]["debit"], 10000)
|
||||
|
||||
def test_company_without_permission_is_refused(self):
|
||||
user = "test_tbp_company_permission@example.com"
|
||||
if not frappe.db.exists("User", user):
|
||||
frappe.get_doc(
|
||||
{"doctype": "User", "email": user, "first_name": "TBP", "roles": [{"role": "Accounts User"}]}
|
||||
).insert()
|
||||
frappe.permissions.add_user_permission("Company", "_Test Company 1", user)
|
||||
|
||||
frappe.set_user(user)
|
||||
try:
|
||||
self.assertRaises(frappe.PermissionError, self.run_report)
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_balances_follow_gl_entry_permissions(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
other_receivable = create_account(
|
||||
account_name="_Test TBP Receivable",
|
||||
company="_Test Company",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
account_type="Receivable",
|
||||
)
|
||||
create_sales_invoice(customer="_Test Customer", qty=1, rate=10000, posting_date="2026-06-01")
|
||||
create_sales_invoice(
|
||||
customer="_Test Customer", qty=1, rate=6000, posting_date="2026-06-01", debit_to=other_receivable
|
||||
)
|
||||
|
||||
user = "test_tbp_account_permission@example.com"
|
||||
if not frappe.db.exists("User", user):
|
||||
frappe.get_doc(
|
||||
{"doctype": "User", "email": user, "first_name": "TBP", "roles": [{"role": "Accounts User"}]}
|
||||
).insert()
|
||||
frappe.permissions.add_user_permission("Account", "Debtors - _TC", user)
|
||||
|
||||
frappe.set_user(user)
|
||||
try:
|
||||
row = self.party_row("_Test Customer")
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
self.assertEqual(row["debit"], 10000)
|
||||
|
||||
def test_alternative_finance_book_entries_excluded(self):
|
||||
customer = "_Test Customer"
|
||||
create_sales_invoice(customer=customer, qty=1, rate=10000, posting_date="2026-06-01")
|
||||
self.make_finance_book_credit(customer, 1500, "2025-12-15")
|
||||
self.make_finance_book_credit(customer, 4000, "2026-06-10")
|
||||
|
||||
row = self.party_row(customer, include_default_book_entries=1)
|
||||
self.assertEqual((row["opening_credit"], row["credit"], row["closing_debit"]), (0, 0, 10000))
|
||||
|
||||
row = self.party_row(customer, finance_book="Test Finance Book 1")
|
||||
self.assertEqual((row["opening_credit"], row["credit"], row["closing_debit"]), (1500, 4000, 4500))
|
||||
|
||||
def make_finance_book_credit(self, customer: str, amount: float, posting_date: str) -> None:
|
||||
journal = make_journal_entry(
|
||||
"Sales - _TC", "Debtors - _TC", amount, posting_date=posting_date, save=False
|
||||
)
|
||||
journal.accounts[1].update({"party_type": "Customer", "party": customer})
|
||||
journal.finance_book = "Test Finance Book 1"
|
||||
journal.submit()
|
||||
@@ -4,15 +4,25 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
from frappe.query_builder.functions import Sum
|
||||
<<<<<<< HEAD
|
||||
from frappe.utils import cint, flt
|
||||
=======
|
||||
from frappe.utils import cint, cstr, flt
|
||||
from pypika.terms import Bracket, LiteralValue
|
||||
>>>>>>> f0c96eb (fix(accounts): apply user permissions in party balance reports)
|
||||
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_accounts_with_children
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import (
|
||||
validate_company_permission,
|
||||
validate_filters,
|
||||
)
|
||||
from erpnext.accounts.utils import get_currency_precision
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
validate_company_permission(filters.company)
|
||||
validate_filters(filters)
|
||||
|
||||
show_party_name = is_party_name_visible(filters)
|
||||
@@ -127,6 +137,15 @@ def get_opening_balances(filters, account_filter=None):
|
||||
if account_filter:
|
||||
query = query.where(GL_Entry.account.isin(account_filter))
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
if finance_book_condition := get_finance_book_condition(GL_Entry, filters):
|
||||
query = query.where(finance_book_condition)
|
||||
|
||||
if match_conditions := build_match_conditions("GL Entry"):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
>>>>>>> f0c96eb (fix(accounts): apply user permissions in party balance reports)
|
||||
gle = query.run(as_dict=True)
|
||||
|
||||
opening = frappe._dict()
|
||||
@@ -162,6 +181,15 @@ def get_balances_within_period(filters, account_filter=None):
|
||||
if account_filter:
|
||||
query = query.where(GL_Entry.account.isin(account_filter))
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
if finance_book_condition := get_finance_book_condition(GL_Entry, filters):
|
||||
query = query.where(finance_book_condition)
|
||||
|
||||
if match_conditions := build_match_conditions("GL Entry"):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
>>>>>>> f0c96eb (fix(accounts): apply user permissions in party balance reports)
|
||||
gle = query.run(as_dict=True)
|
||||
|
||||
balances_within_period = frappe._dict()
|
||||
|
||||
Reference in New Issue
Block a user