perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled (#55130)

* perf: get payment ledger and remove update from delink when immutable ledger is enabled

* revert: changes of get_payment_ledger_entries

* perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled

* test: for immutable ledger

* test: add posting_date in create_sales_invoice

* fix: link validation err with immutable ledger on

* test: update testcase of the immutable ledger

* refactor(test): simpler test for immutable invariants

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
(cherry picked from commit 9eeccecd30)

# Conflicts:
#	erpnext/accounts/general_ledger.py
This commit is contained in:
Nihantra C. Patel
2026-05-22 12:32:53 +05:30
committed by ruthra kumar
parent 498bd2fb4b
commit 8a4cb28d90
3 changed files with 88 additions and 6 deletions

View File

@@ -3,7 +3,8 @@
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.utils import nowdate from frappe.query_builder.functions import Count, Sum
from frappe.utils import add_days, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
@@ -90,6 +91,7 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
posting_date = nowdate() posting_date = nowdate()
sinv = create_sales_invoice( sinv = create_sales_invoice(
posting_date=posting_date,
qty=qty, qty=qty,
rate=rate, rate=rate,
company=self.company, company=self.company,
@@ -531,3 +533,82 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
# with references removed, deletion should be possible # with references removed, deletion should be possible
so.delete() so.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name) self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name)
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{"enable_immutable_ledger": 1},
)
def test_reverse_entries_on_cancel_for_immutable_ledger(self):
invoice_posting_date = add_days(nowdate(), -5)
gle = qb.DocType("GL Entry")
ple = qb.DocType("Payment Ledger Entry")
si = self.create_sales_invoice(qty=1, rate=100, posting_date=invoice_posting_date)
gles_before = (
qb.from_(gle)
.select(
Count(gle.name),
)
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
.run()[0][0]
)
ples_before = (
qb.from_(ple)
.select(
Count(ple.name),
)
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0)))
.run()[0][0]
)
si.cancel()
gles_after = (
qb.from_(gle)
.select(Count(gle.account))
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
.run()[0][0]
)
self.assertEqual(gles_after, gles_before * 2)
ples_after = (
qb.from_(ple)
.select(
Count(ple.name),
)
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0)))
.run()[0][0]
)
self.assertEqual(ples_after, ples_before * 2)
# assert debit/credit are reversed
gl_entries = (
qb.from_(gle)
.select(gle.account, Sum(gle.debit).as_("total_debit"), Sum(gle.credit).as_("total_credit"))
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
.groupby(gle.account)
.run(as_dict=True)
)
for gl in gl_entries:
with self.subTest(gl=gl):
self.assertEqual(gl.total_debit, gl.total_credit)
# assert amounts are reversed
pl_entries = (
qb.from_(ple)
.select(ple.account, Sum(ple.amount).as_("total_amount"))
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked == 0))
.groupby(ple.account)
.run(as_dict=True)
)
for pl in pl_entries:
with self.subTest(pl=pl):
self.assertEqual(pl.total_amount, 0)
self.assertFalse(
frappe.db.exists(
"Payment Ledger Entry",
{"voucher_type": si.doctype, "voucher_no": si.name, "delinked": 1},
)
)

View File

@@ -431,6 +431,8 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.flags.adv_adj = adv_adj gle.flags.adv_adj = adv_adj
gle.flags.update_outstanding = update_outstanding or "Yes" gle.flags.update_outstanding = update_outstanding or "Yes"
gle.flags.notify_update = False gle.flags.notify_update = False
if gle.is_cancelled or is_immutable_ledger_enabled():
gle.flags.ignore_links = True
gle.submit() gle.submit()
if ( if (

View File

@@ -2130,8 +2130,9 @@ def create_payment_ledger_entry(
ple = frappe.get_doc(entry) ple = frappe.get_doc(entry)
if cancel: if cancel:
delink_original_entry(ple, partial_cancel=partial_cancel) if not is_immutable_ledger_enabled():
if is_immutable_ledger_enabled(): delink_original_entry(ple, partial_cancel=partial_cancel)
else:
ple.delinked = 0 ple.delinked = 0
ple.posting_date = frappe.form_dict.get("posting_date") or getdate() ple.posting_date = frappe.form_dict.get("posting_date") or getdate()
@@ -2220,6 +2221,7 @@ def delink_original_entry(pl_entry, partial_cancel=False):
qb.update(ple) qb.update(ple)
.set(ple.modified, now()) .set(ple.modified, now())
.set(ple.modified_by, frappe.session.user) .set(ple.modified_by, frappe.session.user)
.set(ple.delinked, True)
.where( .where(
(ple.company == pl_entry.company) (ple.company == pl_entry.company)
& (ple.account_type == pl_entry.account_type) & (ple.account_type == pl_entry.account_type)
@@ -2236,9 +2238,6 @@ def delink_original_entry(pl_entry, partial_cancel=False):
if partial_cancel: if partial_cancel:
query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no) query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no)
if not is_immutable_ledger_enabled():
query = query.set(ple.delinked, True)
query.run() query.run()