Skip to content

Commit

Permalink
Merge pull request #39802 from frappe/mergify/bp/version-14/pr-39783
Browse files Browse the repository at this point in the history
fix: cancelling cr/dr notes should update the linked Invoice status (backport #39783)
  • Loading branch information
ruthra-kumar authored Feb 8, 2024
2 parents 7413c8b + 8888ce1 commit e452e42
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,70 @@ 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 and 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)
# 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()
amount = 100
Expand Down
20 changes: 20 additions & 0 deletions erpnext/controllers/accounts_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)

Expand Down
14 changes: 7 additions & 7 deletions erpnext/controllers/tests/test_accounts_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down

0 comments on commit e452e42

Please sign in to comment.