From 80422d2108137ddafc171bf0d38b025204952bbf Mon Sep 17 00:00:00 2001 From: pandiyan Date: Wed, 12 Aug 2026 15:59:16 +0530 Subject: [PATCH 1/2] test(accounts): cover reversal of a reverse journal entry also assert that a user without read access on the entry gets a permission error instead of the reversal relationship. --- .../journal_entry/test_journal_entry.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index de773579dce..9257e41d8e9 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -301,6 +301,27 @@ class TestJournalEntry(ERPNextTestSuite): self.check_gl_entries() + def test_disallow_reversal_of_a_reversal_journal_entry(self): + from erpnext.accounts.doctype.journal_entry.mapper import make_reverse_journal_entry + + jv = make_journal_entry("_Test Bank - _TC", "Sales - _TC", 100, submit=True) + + rjv = make_reverse_journal_entry(jv.name) + rjv.posting_date = nowdate() + rjv.submit() + + self.assertRaisesRegex( + frappe.ValidationError, + "is already a Reverse Journal Entry", + make_reverse_journal_entry, + rjv.name, + ) + + # the guard must not disclose the reversal to a user who cannot read the entry + frappe.set_user("Guest") + self.addCleanup(frappe.set_user, "Administrator") + self.assertRaises(frappe.PermissionError, make_reverse_journal_entry, rjv.name) + def test_disallow_change_in_account_currency_for_a_party(self): # create jv in USD jv = make_journal_entry("_Test Bank USD - _TC", "_Test Receivable USD - _TC", 100, save=False) From 9dd37d5f32b739be66123d81f4cd592746ca3803 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Wed, 12 Aug 2026 15:59:16 +0530 Subject: [PATCH 2/2] fix(accounts): disallow reversing a reverse journal entry check read permission on the source entry before the guards run, so the reversal relationship is not disclosed to a user who cannot read it. --- .../doctype/journal_entry/journal_entry.js | 2 +- erpnext/accounts/doctype/journal_entry/mapper.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index dd53f28e01e..cf69f1bab4d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -249,7 +249,7 @@ Object.assign(erpnext.journal_entry, { ); } - if (frm.doc.docstatus == 1) { + if (frm.doc.docstatus == 1 && !frm.doc.reversal_of) { frm.add_custom_button( __("Reverse Journal Entry"), () => erpnext.journal_entry.reverse_journal_entry(frm), diff --git a/erpnext/accounts/doctype/journal_entry/mapper.py b/erpnext/accounts/doctype/journal_entry/mapper.py index 17671cd3ab0..715eecbd398 100644 --- a/erpnext/accounts/doctype/journal_entry/mapper.py +++ b/erpnext/accounts/doctype/journal_entry/mapper.py @@ -222,6 +222,20 @@ def make_inter_company_journal_entry(name: str, voucher_type: str, company: str) @frappe.whitelist() def make_reverse_journal_entry(source_name: str, target_doc: str | dict | Document | None = None) -> Document: """Map a submitted Journal Entry to a reversing one (debits and credits swapped).""" + # `get_mapped_doc` checks this as well, but the guards below disclose which entry + # reverses which, so read access has to be settled before they run + if not frappe.has_permission("Journal Entry", doc=source_name): + frappe.throw(_("Not permitted"), frappe.PermissionError) + + reversal_of = frappe.db.get_value("Journal Entry", source_name, "reversal_of") + if reversal_of: + frappe.throw( + _("{0} is already a Reverse Journal Entry of {1}. Cancel it instead of reversing it.").format( + get_link_to_form("Journal Entry", source_name), + get_link_to_form("Journal Entry", reversal_of), + ) + ) + existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1}) if existing_reverse: frappe.throw(