From 5c59ab5975229ce36cecd6f655f61a5f246bae34 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 16:32:23 +0530 Subject: [PATCH 1/4] refactor: cancel Cr/Dr JE's on Sales/Purchase return cancel (cherry picked from commit 0549535603cae6a7afbd3375c8dd62b517af3c6e) --- erpnext/controllers/accounts_controller.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a3f48141aaca..2420d987c233 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1374,6 +1374,24 @@ def update_against_document_in_jv(self): x.update({dim.fieldname: self.get(dim.fieldname)}) reconcile_against_document(lst, active_dimensions=active_dimensions) + def cancel_system_generated_credit_debit_notes(self): + # Cancel 'Credit/Debit' Note Journal Entries, if found. + if self.doctype in ["Sales Invoice", "Purchase Invoice"]: + voucher_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "reference_type": self.doctype, + "reference_name": self.name, + "voucher_type": voucher_type, + "docstatus": 1, + }, + pluck="name", + ) + for x in journals: + frappe.get_doc("Journal Entry", x).cancel() + def on_cancel(self): from erpnext.accounts.doctype.bank_transaction.bank_transaction import ( remove_from_bank_transaction, @@ -1386,6 +1404,8 @@ def on_cancel(self): remove_from_bank_transaction(self.doctype, self.name) if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: + self.cancel_system_generated_credit_debit_notes() + # Cancel Exchange Gain/Loss Journal before unlinking cancel_exchange_gain_loss_journal(self) From c419c1de06114ef8fa442d49dadb3cbc7dfd8b04 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 17:21:29 +0530 Subject: [PATCH 2/4] test: Invoice status on Cr/Dr note cancellation (cherry picked from commit 31a8c3bdc45f0e32a51e43260db8484e5f17aa75) --- .../test_payment_reconciliation.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index d7a73f0ce714..89240ac0d8ad 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -591,6 +591,66 @@ def test_cr_note_against_invoice(self): self.assertEqual(si.status, "Paid") self.assertEqual(si.outstanding_amount, 0) + def test_invoice_status_after_cr_note_cancellation(self): + # This test case is made after the 'always standalone Credit/Debit notes' feature is introduced + transaction_date = nowdate() + amount = 100 + + si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + + cr_note = self.create_sales_invoice( + qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True + ) + cr_note.is_return = 1 + cr_note.return_against = si.name + cr_note = cr_note.save().submit() + + pr = self.create_payment_reconciliation() + + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + pr.get_unreconciled_entries() + self.assertEqual(pr.get("invoices"), []) + self.assertEqual(pr.get("payments"), []) + + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "docstatus": 1, + "voucher_type": "Credit Note", + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="name", + ) + self.assertEqual(len(journals), 1) + + # assert status outstanding + si.reload() + self.assertEqual(si.status, "Credit Note Issued") + self.assertEqual(si.outstanding_amount, 0) + + cr_note.reload() + cr_note.cancel() + # 'Credit Note' Journal should be auto cancelled + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "docstatus": 1, + "voucher_type": "Credit Note", + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="name", + ) + self.assertEqual(len(journals), 0) + def test_cr_note_partial_against_invoice(self): transaction_date = nowdate() amount = 100 From 129ab38fba27f915c47ccce1eecbcbe29c7d0e12 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 17:33:55 +0530 Subject: [PATCH 3/4] refactor(test): assert Invoice status as well (cherry picked from commit 33efe0d12d85484e551dc9ebebf8840d427ecc67) --- .../payment_reconciliation/test_payment_reconciliation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 89240ac0d8ad..fb75a0f7caf9 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -630,7 +630,7 @@ def test_invoice_status_after_cr_note_cancellation(self): ) self.assertEqual(len(journals), 1) - # assert status outstanding + # assert status and outstanding si.reload() self.assertEqual(si.status, "Credit Note Issued") self.assertEqual(si.outstanding_amount, 0) @@ -650,6 +650,10 @@ def test_invoice_status_after_cr_note_cancellation(self): pluck="name", ) self.assertEqual(len(journals), 0) + # assert status and outstanding + si.reload() + self.assertEqual(si.status, "Unpaid") + self.assertEqual(si.outstanding_amount, 100) def test_cr_note_partial_against_invoice(self): transaction_date = nowdate() From 8888ce196df86dc850fe9287cc84611a2aedcced Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 19:59:33 +0530 Subject: [PATCH 4/4] refactor(test): Forex Credit Note cancellation against Invoice (cherry picked from commit 2f676ced5c712823c5737f40230ec8b1994cd2dd) --- .../controllers/tests/test_accounts_controller.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 5e3077ea8c86..0c9d34d82a22 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1108,18 +1108,18 @@ def test_30_cr_note_against_sales_invoice(self): cr_note.reload() cr_note.cancel() - # Exchange Gain/Loss Journal should've been created. + # with the introduction of 'cancel_system_generated_credit_debit_notes' in accounts controller + # JE(Credit Note) will be cancelled once the parent is cancelled exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name) - self.assertNotEqual(exc_je_for_si, []) - self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 0) self.assertEqual(len(exc_je_for_cr), 0) - # The Credit Note JE is still active and is referencing the sales invoice - # So, outstanding stays the same + # No references, full outstanding si.reload() - self.assertEqual(si.outstanding_amount, 1) - self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) def test_40_cost_center_from_payment_entry(self): """