diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 2de009f8c43..536cdba565c 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +import json import unittest import frappe @@ -21,6 +22,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( create_sales_invoice, create_sales_invoice_against_cost_center, ) +from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.setup.doctype.employee.test_employee import make_employee @@ -1219,6 +1221,76 @@ class TestPaymentEntry(FrappeTestCase): so.reload() self.assertEqual(so.advance_paid, so.rounded_total) + def test_partial_cancel_for_payment_entry(self): + si = create_sales_invoice() + + pe = get_payment_entry(si.doctype, si.name) + pe.save() + pe.submit() + + # Additional GL Entry + tax_amount = 10 + reference_row = pe.references[0] + gl_args = { + "party_type": pe.party_type, + "party": pe.party, + "against_voucher_type": reference_row.reference_doctype, + "against_voucher": reference_row.reference_name, + "voucher_detail_no": reference_row.name, + } + + gl_dicts = [] + + gl_dicts.extend( + [ + pe.get_gl_dict( + { + "account": pe.paid_to, + "credit": tax_amount, + "credit_in_account_currency": tax_amount, + **gl_args, + } + ), + pe.get_gl_dict( + { + "account": pe.paid_from, + "debit": tax_amount, + "debit_in_account_currency": tax_amount, + **gl_args, + } + ), + ] + ) + + make_gl_entries(gl_dicts) + + # Assert PLEs Before + self.assertPLEntries( + pe, + [ + {"amount": -100.0, "against_voucher_no": si.name}, + {"amount": 10.0, "against_voucher_no": si.name}, + ], + ) + + # Partially cancel Payment Entry + make_reverse_gl_entries(gl_dicts, partial_cancel=True) + self.assertPLEntries(pe, [{"amount": -100.0, "against_voucher_no": si.name}]) + + def assertPLEntries(self, payment_doc, expected_pl_entries): + pl_entries = frappe.get_all( + "Payment Ledger Entry", + filters={ + "voucher_type": payment_doc.doctype, + "voucher_no": payment_doc.name, + "delinked": 0, + }, + fields=["amount", "against_voucher_no"], + ) + out_str = json.dumps(sorted(pl_entries, key=json.dumps)) + expected_out_str = json.dumps(sorted(expected_pl_entries, key=json.dumps)) + self.assertEqual(out_str, expected_out_str) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 07cc5dd734f..134ddddf9e0 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -556,7 +556,12 @@ def get_round_off_account_and_cost_center( def make_reverse_gl_entries( - gl_entries=None, voucher_type=None, voucher_no=None, adv_adj=False, update_outstanding="Yes" + gl_entries=None, + voucher_type=None, + voucher_no=None, + adv_adj=False, + update_outstanding="Yes", + partial_cancel=False, ): """ Get original gl entries of the voucher @@ -576,14 +581,19 @@ def make_reverse_gl_entries( if gl_entries: create_payment_ledger_entry( - gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding + gl_entries, + cancel=1, + adv_adj=adv_adj, + update_outstanding=update_outstanding, + partial_cancel=partial_cancel, ) validate_accounting_period(gl_entries) check_freezing_date(gl_entries[0]["posting_date"], adv_adj) is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries) validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"]) - set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) + if not partial_cancel: + set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) for entry in gl_entries: new_gle = copy.deepcopy(entry) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 966e7e4bf1e..3d426919fcf 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1617,6 +1617,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0): due_date=gle.due_date, voucher_type=gle.voucher_type, voucher_no=gle.voucher_no, + voucher_detail_no=gle.voucher_detail_no, against_voucher_type=gle.against_voucher_type if gle.against_voucher_type else gle.voucher_type, @@ -1638,7 +1639,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0): def create_payment_ledger_entry( - gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0 + gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0, partial_cancel=False ): if gl_entries: ple_map = get_payment_ledger_entries(gl_entries, cancel=cancel) @@ -1648,7 +1649,7 @@ def create_payment_ledger_entry( ple = frappe.get_doc(entry) if cancel: - delink_original_entry(ple) + delink_original_entry(ple, partial_cancel=partial_cancel) ple.flags.ignore_permissions = 1 ple.flags.adv_adj = adv_adj @@ -1695,7 +1696,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa ref_doc.set_status(update=True) -def delink_original_entry(pl_entry): +def delink_original_entry(pl_entry, partial_cancel=False): if pl_entry: ple = qb.DocType("Payment Ledger Entry") query = ( @@ -1715,6 +1716,10 @@ def delink_original_entry(pl_entry): & (ple.against_voucher_no == pl_entry.against_voucher_no) ) ) + + if partial_cancel: + query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no) + query.run()