diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index 13c2ba29a37..c5b4623d8da 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -22,6 +22,7 @@ from erpnext.accounts.report.financial_statements import ( get_period_list, prepare_data, ) +from erpnext.accounts.report.utils import has_gl_entry_restrictions def execute(filters=None): @@ -271,6 +272,9 @@ def get_chart_data(filters, chart_columns, asset, liability, equity, currency): def execute_snapshot_report(filters): + if has_gl_entry_restrictions(): + return execute(filters) + from frappe.database.duckdb.database import get_latest_sync if not (conn := get_latest_sync("GL Entry")): diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index daee573615b..2438f633142 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -16,7 +16,11 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_dimension_with_children, ) from erpnext.accounts.report.financial_statements import get_cost_centers_with_children -from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency +from erpnext.accounts.report.utils import ( + convert_to_presentation_currency, + get_currency, + has_gl_entry_restrictions, +) from erpnext.accounts.utils import get_account_currency @@ -795,6 +799,9 @@ def get_columns(filters): def execute_snapshot_report(filters): + if has_gl_entry_restrictions(): + return execute(filters) + from frappe.database.duckdb.database import get_latest_sync if conn := get_latest_sync("GL Entry"): diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py index d2a7e8256b3..65a16841543 100644 --- a/erpnext/accounts/report/general_ledger/test_general_ledger.py +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -12,6 +12,33 @@ from erpnext.controllers.sales_and_purchase_return import make_return_doc class TestGeneralLedger(FrappeTestCase): + def test_snapshot_reports_use_live_data_for_restricted_users(self): + from importlib import import_module + from unittest.mock import patch + + from erpnext.tests.permission_test_utils import as_user, make_fenced_user + + restricted = make_fenced_user( + "gl-snapshot-fenced@example.com", ["Accounts User"], [("Company", "_Test Company")] + ) + unrestricted = make_fenced_user("gl-snapshot-open@example.com", ["Accounts User"]) + for report in ("general_ledger", "balance_sheet", "profit_and_loss_statement", "trial_balance"): + module = import_module(f"erpnext.accounts.report.{report}.{report}") + for user, expect_live in ((restricted, True), (unrestricted, False)): + with ( + as_user(user), + patch.object(module, "execute", return_value="live") as live, + patch( + "frappe.database.duckdb.database.get_latest_sync", side_effect=frappe.ValidationError + ), + ): + try: + result = module.execute_snapshot_report(frappe._dict(company="_Test Company")) + except frappe.ValidationError: + result = "snapshot" + self.assertEqual(result, "live" if expect_live else "snapshot", f"{report} as {user}") + self.assertEqual(live.called, expect_live) + def setUp(self): self.company = "_Test Company" self.clear_old_entries() diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index f0f41d063cc..932d5c4996e 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -22,6 +22,7 @@ from erpnext.accounts.report.financial_statements import ( get_period_list, prepare_data, ) +from erpnext.accounts.report.utils import has_gl_entry_restrictions def execute(filters=None): @@ -204,6 +205,9 @@ def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, cur def execute_snapshot_report(filters): + if has_gl_entry_restrictions(): + return execute(filters) + from frappe.database.duckdb.database import get_latest_sync if not (conn := get_latest_sync("GL Entry")): diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 48e2ff039a5..25743dc559b 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -18,7 +18,11 @@ from erpnext.accounts.report.financial_statements import ( get_cost_centers_with_children, set_gl_entries_by_account, ) -from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency +from erpnext.accounts.report.utils import ( + convert_to_presentation_currency, + get_currency, + has_gl_entry_restrictions, +) from erpnext.accounts.utils import get_zero_cutoff value_fields = ( @@ -513,6 +517,9 @@ def hide_group_accounts(data): def execute_snapshot_report(filters): + if has_gl_entry_restrictions(): + return execute(filters) + from frappe.database.duckdb.database import get_latest_sync if conn := get_latest_sync("GL Entry"): diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 189ac56c874..d7878e67b44 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -436,3 +436,10 @@ def get_opening_row(party_type, party, from_date, company): & (gle.is_cancelled == 0) ) ).run(as_dict=True) + + +def has_gl_entry_restrictions() -> bool: + """Snapshot reports read a DuckDB copy that cannot apply the user's GL Entry permission conditions.""" + from frappe.desk.reportview import build_match_conditions + + return bool(build_match_conditions("GL Entry"))