Merge pull request #39783 from ruthra-kumar/cancel_cr_dr_note_jes_on_cancel

fix: cancelling cr/dr notes should update the linked Invoice status
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index d7a73f0..fb75a0f 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -591,6 +591,70 @@
 		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
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index e2b0ee5..308a134 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1476,6 +1476,24 @@
 						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,
@@ -1488,6 +1506,8 @@
 		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)
 
diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py
index fad216d..d2a3574 100644
--- a/erpnext/controllers/tests/test_accounts_controller.py
+++ b/erpnext/controllers/tests/test_accounts_controller.py
@@ -1108,18 +1108,18 @@
 		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):
 		"""