From 5445640fefe1a9ba9c2011ed075aea3282d7b475 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 30 Jun 2025 14:18:40 +0530 Subject: [PATCH] fix: update payment request outstanding on unreconciliation (cherry picked from commit 8098229b55c982dccc6a9aafc4ac4b3e8650d7cb) # Conflicts: # erpnext/accounts/doctype/payment_request/test_payment_request.py # erpnext/accounts/utils.py --- .../payment_request/payment_request.py | 3 +- .../payment_request/test_payment_request.py | 139 ++++++++++++++++++ erpnext/accounts/utils.py | 94 ++++++++---- 3 files changed, 209 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 1012ff56ebf..90727f29d56 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -698,8 +698,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False): if not references: return - precision = references[0].precision("allocated_amount") - + precision = frappe.get_precision("Payment Entry Reference", "allocated_amount") referenced_payment_requests = frappe.get_all( "Payment Request", filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]}, diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index b0c3dbf4d5b..3ed8e9f0bb8 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -524,3 +524,142 @@ class TestPaymentRequest(FrappeTestCase): self.assertEqual(pr.grand_total, 1000) so.load_from_db() +<<<<<<< HEAD +======= + self.assertEqual(so.advance_payment_status, "Requested") + + def test_partial_paid_invoice_with_payment_request(self): + si = create_sales_invoice(currency="INR", qty=1, rate=5000) + si.save() + si.submit() + + pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") + pe.reference_no = "PAYEE0002" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 2500 + pe.references[0].allocated_amount = 2500 + pe.save() + pe.submit() + + si.load_from_db() + pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1) + + self.assertEqual(pr.grand_total, si.outstanding_amount) + + def test_partial_paid_invoice_with_more_payment_entry(self): + pi = make_purchase_invoice(currency="INR", qty=1, rate=500) + pi.submit() + pi_1 = make_purchase_invoice(currency="INR", qty=1, rate=300) + pi_1.submit() + + pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1) + pr.grand_total = 200 + pr.submit() + pr.create_payment_entry() + pr_1 = make_payment_request( + dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1 + ) + pr_1.grand_total = 200 + pr_1.submit() + pr_1.create_payment_entry() + + pe = get_payment_entry(dt="Purchase Invoice", dn=pi.name) + pe.paid_amount = 200 + pe.references[0].reference_doctype = pi.doctype + pe.references[0].reference_name = pi.name + pe.references[0].grand_total = pi.grand_total + pe.references[0].outstanding_amount = pi.outstanding_amount + pe.references[0].allocated_amount = 100 + pe.append( + "references", + { + "reference_doctype": pi_1.doctype, + "reference_name": pi_1.name, + "grand_total": pi_1.grand_total, + "outstanding_amount": pi_1.outstanding_amount, + "allocated_amount": 100, + }, + ) + + pr_2 = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1) + pi.load_from_db() + self.assertEqual(pr_2.grand_total, pi.outstanding_amount) + + def test_consider_journal_entry_and_return_invoice(self): + from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry + + si = create_sales_invoice(currency="INR", qty=5, rate=500) + + je = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 500, save=False) + je.accounts[1].party_type = "Customer" + je.accounts[1].party = si.customer + je.accounts[1].reference_type = "Sales Invoice" + je.accounts[1].reference_name = si.name + je.accounts[1].credit_in_account_currency = 500 + je.submit() + + pe = get_payment_entry("Sales Invoice", si.name) + pe.paid_amount = 500 + pe.references[0].allocated_amount = 500 + pe.save() + pe.submit() + + cr_note = create_sales_invoice(qty=-1, rate=500, is_return=1, return_against=si.name, do_not_save=1) + cr_note.update_outstanding_for_self = 0 + cr_note.save() + cr_note.submit() + + si.load_from_db() + pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1) + self.assertEqual(pr.grand_total, si.outstanding_amount) + + def test_partial_paid_invoice_with_submitted_payment_entry(self): + pi = make_purchase_invoice(currency="INR", qty=1, rate=5000) + pi.save() + pi.submit() + + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe.reference_no = "PURINV0001" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 2500 + pe.references[0].allocated_amount = 2500 + pe.save() + pe.submit() + pe.cancel() + + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe.reference_no = "PURINV0002" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 2500 + pe.references[0].allocated_amount = 2500 + pe.save() + pe.submit() + + pi.load_from_db() + pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1) + self.assertEqual(pr.grand_total, pi.outstanding_amount) + + def test_payment_request_on_unreconcile(self): + pi = make_purchase_invoice(currency="INR", qty=1, rate=500) + pi.submit() + + pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1) + self.assertEqual(pr.grand_total, pi.outstanding_amount) + + pe = pr.create_payment_entry() + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payment", + "company": pe.company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + } + ) + unreconcile.add_references() + unreconcile.submit() + + pi.load_from_db() + pr.load_from_db() + + self.assertEqual(pr.grand_total, pi.outstanding_amount) +>>>>>>> 8098229b55 (fix: update payment request outstanding on unreconciliation) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index af760519054..2eccfa8c09b 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -920,30 +920,24 @@ def remove_ref_doc_link_from_pe( per = qb.DocType("Payment Entry Reference") pay = qb.DocType("Payment Entry") - linked_pe = ( + query = ( qb.from_(per) - .select(per.parent) - .where((per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2))) - .run(as_list=1) - ) - linked_pe = convert_to_list(linked_pe) - # remove reference only from specified payment - linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe - - if linked_pe: - update_query = ( - qb.update(per) - .set(per.allocated_amount, 0) - .set(per.modified, now()) - .set(per.modified_by, frappe.session.user) - .where(per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) + .select("*") + .where( + (per.reference_doctype == ref_type) + & (per.reference_name == ref_no) + & (per.docstatus.lt(2)) + & (per.parenttype == "Payment Entry") ) + ) - if payment_name: - update_query = update_query.where(per.parent == payment_name) + # update reference only from specified payment + if payment_name: + query = query.where(per.parent == payment_name) - update_query.run() + reference_rows = query.run(as_dict=True) +<<<<<<< HEAD for pe in linked_pe: try: pe_doc = frappe.get_doc("Payment Entry", pe) @@ -955,14 +949,64 @@ def remove_ref_doc_link_from_pe( msg += "
" msg += _("Please cancel payment entry manually first") frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error")) +======= + if not reference_rows: + return - qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set( - pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount - ).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set( - pay.modified_by, frappe.session.user - ).where(pay.name == pe).run() + linked_pe = set() + row_names = set() - frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe))) + for row in reference_rows: + linked_pe.add(row.parent) + row_names.add(row.name) +>>>>>>> 8098229b55 (fix: update payment request outstanding on unreconciliation) + + from erpnext.accounts.doctype.payment_request.payment_request import ( + update_payment_requests_as_per_pe_references, + ) + + # Update payment request amount + update_payment_requests_as_per_pe_references(reference_rows, cancel=True) + + # Update allocated amounts and modified fields in one go + ( + qb.update(per) + .set(per.allocated_amount, 0) + .set(per.modified, now()) + .set(per.modified_by, frappe.session.user) + .where(per.name.isin(row_names)) + .where(per.parenttype == "Payment Entry") + .run() + ) + + for pe in linked_pe: + try: + pe_doc = frappe.get_doc("Payment Entry", pe) + pe_doc.set_amounts() + + # Call cancel on only removed reference + references = [x for x in pe_doc.references if x.name in row_names] + [pe_doc.make_advance_gl_entries(x, cancel=1) for x in references] + + pe_doc.clear_unallocated_reference_document_rows() + pe_doc.validate_payment_type_with_outstanding() + except Exception: + msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name) + msg += "
" + msg += _("Please cancel payment entry manually first") + frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error")) + + ( + qb.update(pay) + .set(pay.total_allocated_amount, pe_doc.total_allocated_amount) + .set(pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount) + .set(pay.unallocated_amount, pe_doc.unallocated_amount) + .set(pay.modified, now()) + .set(pay.modified_by, frappe.session.user) + .where(pay.name == pe) + .run() + ) + frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe))) @frappe.whitelist()