Merge pull request #40372 from ruthra-kumar/toggle_for_standalone_cr_dr_notes

refactor: checkbox to toggle standalone Credit/Debit note behaviour
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index d12a43c..22f2d13 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -22,6 +22,7 @@
   "is_paid",
   "is_return",
   "return_against",
+  "update_outstanding_for_self",
   "update_billed_amount_in_purchase_order",
   "update_billed_amount_in_purchase_receipt",
   "apply_tds",
@@ -1623,13 +1624,21 @@
    "fieldtype": "Link",
    "label": "Supplier Group",
    "options": "Supplier Group"
+  },
+  {
+   "default": "1",
+   "depends_on": "eval: doc.is_return && doc.return_against",
+   "description": "Debit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
+   "fieldname": "update_outstanding_for_self",
+   "fieldtype": "Check",
+   "label": "Update Outstanding for Self"
   }
  ],
  "icon": "fa fa-file-text",
  "idx": 204,
  "is_submittable": 1,
  "links": [],
- "modified": "2024-02-25 11:20:28.366808",
+ "modified": "2024-03-11 14:46:30.298184",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 8dfd69f..28d4a5e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -217,6 +217,7 @@
 		unrealized_profit_loss_account: DF.Link | None
 		update_billed_amount_in_purchase_order: DF.Check
 		update_billed_amount_in_purchase_receipt: DF.Check
+		update_outstanding_for_self: DF.Check
 		update_stock: DF.Check
 		use_company_roundoff_cost_center: DF.Check
 		use_transaction_date_exchange_rate: DF.Check
@@ -829,6 +830,10 @@
 		)
 
 		if grand_total and not self.is_internal_transfer():
+			against_voucher = self.name
+			if self.is_return and self.return_against and not self.update_outstanding_for_self:
+				against_voucher = self.return_against
+
 			# Did not use base_grand_total to book rounding loss gle
 			gl_entries.append(
 				self.get_gl_dict(
@@ -842,7 +847,7 @@
 						"credit_in_account_currency": base_grand_total
 						if self.party_account_currency == self.company_currency
 						else grand_total,
-						"against_voucher": self.name,
+						"against_voucher": against_voucher,
 						"against_voucher_type": self.doctype,
 						"project": self.project,
 						"cost_center": self.cost_center,
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 88b28ad..ac14d98 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -25,6 +25,7 @@
   "is_consolidated",
   "is_return",
   "return_against",
+  "update_outstanding_for_self",
   "update_billed_amount_in_sales_order",
   "update_billed_amount_in_delivery_note",
   "is_debit_note",
@@ -2171,6 +2172,14 @@
    "fieldtype": "Check",
    "label": "Don't Create Loyalty Points",
    "no_copy": 1
+  },
+  {
+   "default": "1",
+   "depends_on": "eval: doc.is_return && doc.return_against",
+   "description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
+   "fieldname": "update_outstanding_for_self",
+   "fieldtype": "Check",
+   "label": "Update Outstanding for Self"
   }
  ],
  "icon": "fa fa-file-text",
@@ -2183,7 +2192,7 @@
    "link_fieldname": "consolidated_invoice"
   }
  ],
- "modified": "2024-03-01 09:21:54.201289",
+ "modified": "2024-03-11 14:20:34.874192",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Sales Invoice",
@@ -2238,4 +2247,4 @@
  "title_field": "title",
  "track_changes": 1,
  "track_seen": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index e2cbf5e..bf50e77 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -220,6 +220,7 @@
 		unrealized_profit_loss_account: DF.Link | None
 		update_billed_amount_in_delivery_note: DF.Check
 		update_billed_amount_in_sales_order: DF.Check
+		update_outstanding_for_self: DF.Check
 		update_stock: DF.Check
 		use_company_roundoff_cost_center: DF.Check
 		write_off_account: DF.Link | None
@@ -1219,6 +1220,10 @@
 		)
 
 		if grand_total and not self.is_internal_transfer():
+			against_voucher = self.name
+			if self.is_return and self.return_against and not self.update_outstanding_for_self:
+				against_voucher = self.return_against
+
 			# Did not use base_grand_total to book rounding loss gle
 			gl_entries.append(
 				self.get_gl_dict(
@@ -1232,7 +1237,7 @@
 						"debit_in_account_currency": base_grand_total
 						if self.party_account_currency == self.company_currency
 						else grand_total,
-						"against_voucher": self.name,
+						"against_voucher": against_voucher,
 						"against_voucher_type": self.doctype,
 						"cost_center": self.cost_center,
 						"project": self.project,
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 6d77ef5..38723e9 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -690,7 +690,12 @@
 
 	def get_return_entries(self):
 		doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
-		filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
+		filters = {
+			"is_return": 1,
+			"docstatus": 1,
+			"company": self.filters.company,
+			"update_outstanding_for_self": 0,
+		}
 		or_filters = {}
 		for party_type in self.party_type:
 			party_field = scrub(party_type)
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index 6ff81be..a0f8af5 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -62,7 +62,7 @@
 		pe.insert()
 		pe.submit()
 
-	def create_credit_note(self, docname):
+	def create_credit_note(self, docname, do_not_submit=False):
 		credit_note = create_sales_invoice(
 			company=self.company,
 			customer=self.customer,
@@ -72,6 +72,7 @@
 			cost_center=self.cost_center,
 			is_return=1,
 			return_against=docname,
+			do_not_submit=do_not_submit,
 		)
 
 		return credit_note
@@ -149,7 +150,9 @@
 			)
 
 		# check invoice grand total, invoiced, paid and outstanding column's value after credit note
-		self.create_credit_note(si.name)
+		cr_note = self.create_credit_note(si.name, do_not_submit=True)
+		cr_note.update_outstanding_for_self = False
+		cr_note.save().submit()
 		report = execute(filters)
 
 		expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
@@ -167,6 +170,68 @@
 			],
 		)
 
+	def test_cr_note_flag_to_update_self(self):
+		filters = {
+			"company": self.company,
+			"report_date": today(),
+			"range1": 30,
+			"range2": 60,
+			"range3": 90,
+			"range4": 120,
+			"show_remarks": True,
+		}
+
+		# check invoice grand total and invoiced column's value for 3 payment terms
+		si = self.create_sales_invoice(no_payment_schedule=True)
+		name = si.name
+
+		report = execute(filters)
+
+		expected_data = [100, 100, "No Remarks"]
+
+		self.assertEqual(len(report[1]), 1)
+		row = report[1][0]
+		self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
+
+		# check invoice grand total, invoiced, paid and outstanding column's value after payment
+		self.create_payment_entry(si.name)
+		report = execute(filters)
+
+		expected_data_after_payment = [100, 100, 40, 60]
+		self.assertEqual(len(report[1]), 1)
+		row = report[1][0]
+		self.assertEqual(
+			expected_data_after_payment,
+			[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
+		)
+
+		# check invoice grand total, invoiced, paid and outstanding column's value after credit note
+		cr_note = self.create_credit_note(si.name, do_not_submit=True)
+		cr_note.posting_date = add_days(today(), 1)
+		cr_note.update_outstanding_for_self = True
+		cr_note.save().submit()
+		report = execute(filters)
+
+		expected_data_after_credit_note = [
+			[100.0, 100.0, 40.0, 0.0, 60.0, self.debit_to],
+			[0, 0, 100.0, 0.0, -100.0, self.debit_to],
+		]
+		self.assertEqual(len(report[1]), 2)
+		for i in range(2):
+			row = report[1][i - 1]
+			# row = report[1][0]
+			self.assertEqual(
+				expected_data_after_credit_note[i - 1],
+				[
+					row.invoice_grand_total,
+					row.invoiced,
+					row.paid,
+					row.credit_note,
+					row.outstanding,
+					row.party_account,
+				],
+			)
+
 	def test_payment_againt_po_in_receivable_report(self):
 		"""
 		Payments made against Purchase Order will show up as outstanding amount
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index c543dfc..250f21b 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -218,17 +218,18 @@
 				)
 
 			if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
-				# if self.get("is_return") and self.get("return_against"):
-				document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
-				frappe.msgprint(
-					_(
-						"{0} will be treated as a standalone {0}. Post creation use {1} tool to reconcile against {2}."
-					).format(
-						document_type,
-						get_link_to_form("Payment Reconciliation"),
-						get_link_to_form(self.doctype, self.get("return_against")),
+				if self.get("update_outstanding_for_self"):
+					document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
+					frappe.msgprint(
+						_(
+							"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox. <br><br> Or you can use {3} tool to reconcile against {1} later."
+						).format(
+							frappe.bold(document_type),
+							get_link_to_form(self.doctype, self.get("return_against")),
+							frappe.bold("Update Outstanding for Self"),
+							get_link_to_form("Payment Reconciliation"),
+						)
 					)
-				)
 
 			pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
 			if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 8033289..15dfc36 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -356,6 +356,7 @@
 erpnext.patches.v15_0.create_advance_payment_status
 erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
 erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
+erpnext.patches.v14_0.update_flag_for_return_invoices
 # below migration patch should always run last
 erpnext.patches.v14_0.migrate_gl_to_payment_ledger
 erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
diff --git a/erpnext/patches/v14_0/update_flag_for_return_invoices.py b/erpnext/patches/v14_0/update_flag_for_return_invoices.py
new file mode 100644
index 0000000..feb43be
--- /dev/null
+++ b/erpnext/patches/v14_0/update_flag_for_return_invoices.py
@@ -0,0 +1,62 @@
+from frappe import qb
+
+
+def execute():
+	# Set "update_outstanding_for_self" flag in Credit/Debit Notes
+	# Fetch Credit/Debit notes that does have 'return_against' but still post ledger entries against themselves.
+
+	gle = qb.DocType("GL Entry")
+
+	# Use hardcoded 'creation' date to isolate Credit/Debit notes created post v14 backport
+	# https://github.com/frappe/erpnext/pull/39497
+	creation_date = "2024-01-25"
+
+	si = qb.DocType("Sales Invoice")
+	if cr_notes := (
+		qb.from_(si)
+		.select(si.name)
+		.where(
+			(si.creation.gte(creation_date))
+			& (si.docstatus == 1)
+			& (si.is_return == True)
+			& (si.return_against.notnull())
+		)
+		.run()
+	):
+		cr_notes = [x[0] for x in cr_notes]
+		if docs_that_require_update := (
+			qb.from_(gle)
+			.select(gle.voucher_no)
+			.distinct()
+			.where((gle.voucher_no.isin(cr_notes)) & (gle.voucher_no == gle.against_voucher))
+			.run()
+		):
+			docs_that_require_update = [x[0] for x in docs_that_require_update]
+			qb.update(si).set(si.update_outstanding_for_self, True).where(
+				si.name.isin(docs_that_require_update)
+			).run()
+
+	pi = qb.DocType("Purchase Invoice")
+	if dr_notes := (
+		qb.from_(pi)
+		.select(pi.name)
+		.where(
+			(pi.creation.gte(creation_date))
+			& (pi.docstatus == 1)
+			& (pi.is_return == True)
+			& (pi.return_against.notnull())
+		)
+		.run()
+	):
+		dr_notes = [x[0] for x in dr_notes]
+		if docs_that_require_update := (
+			qb.from_(gle)
+			.select(gle.voucher_no)
+			.distinct()
+			.where((gle.voucher_no.isin(dr_notes)) & (gle.voucher_no == gle.against_voucher))
+			.run()
+		):
+			docs_that_require_update = [x[0] for x in docs_that_require_update]
+			qb.update(pi).set(pi.update_outstanding_for_self, True).where(
+				pi.name.isin(docs_that_require_update)
+			).run()