Merge pull request #35604 from pps190/fix-reconcile-invoice-return

fix: reconcile invoice against credit note.
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 2e4e3b0..a709740 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -336,6 +336,7 @@
 
 		entry_list = []
 		dr_or_cr_notes = []
+		difference_entries = []
 		for row in self.get("allocation"):
 			reconciled_entry = []
 			if row.invoice_number and row.allocated_amount:
@@ -348,13 +349,15 @@
 				reconciled_entry.append(payment_details)
 
 				if payment_details.difference_amount:
-					self.make_difference_entry(payment_details)
+					difference_entries.append(
+						self.make_difference_entry(payment_details, do_not_save_and_submit=bool(dr_or_cr_notes))
+					)
 
 		if entry_list:
 			reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
 
 		if dr_or_cr_notes:
-			reconcile_dr_cr_note(dr_or_cr_notes, self.company)
+			reconcile_dr_cr_note(dr_or_cr_notes, difference_entries, self.company)
 
 	@frappe.whitelist()
 	def reconcile(self):
@@ -382,7 +385,7 @@
 
 		self.get_unreconciled_entries()
 
-	def make_difference_entry(self, row):
+	def make_difference_entry(self, row, do_not_save_and_submit=False):
 		journal_entry = frappe.new_doc("Journal Entry")
 		journal_entry.voucher_type = "Exchange Gain Or Loss"
 		journal_entry.company = self.company
@@ -430,8 +433,11 @@
 
 		journal_entry.append("accounts", journal_account)
 
-		journal_entry.save()
-		journal_entry.submit()
+		if not do_not_save_and_submit:
+			journal_entry.save()
+			journal_entry.submit()
+
+		return journal_entry
 
 	def get_payment_details(self, row, dr_or_cr):
 		return frappe._dict(
@@ -597,7 +603,14 @@
 		return condition
 
 
-def reconcile_dr_cr_note(dr_cr_notes, company):
+def reconcile_dr_cr_note(dr_cr_notes, difference_entries, company):
+	def find_difference_entry(voucher_type, voucher_no):
+		for jv in difference_entries:
+			accounts = iter(jv.accounts)
+			for account in accounts:
+				if account.reference_type == voucher_type and account.reference_name == voucher_no:
+					return next(accounts)
+
 	for inv in dr_cr_notes:
 		voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
 
@@ -642,5 +655,9 @@
 				],
 			}
 		)
+
+		if difference_entry := find_difference_entry(inv.against_voucher_type, inv.against_voucher):
+			jv.append("accounts", difference_entry)
+
 		jv.flags.ignore_mandatory = True
 		jv.submit()
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 3be11ae..2ac7df0 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -11,10 +11,13 @@
 from erpnext import get_default_cost_center
 from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
 from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
 from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
 from erpnext.accounts.party import get_party_account
 from erpnext.stock.doctype.item.test_item import create_item
 
+test_dependencies = ["Item"]
+
 
 class TestPaymentReconciliation(FrappeTestCase):
 	def setUp(self):
@@ -163,7 +166,9 @@
 	def create_payment_reconciliation(self):
 		pr = frappe.new_doc("Payment Reconciliation")
 		pr.company = self.company
-		pr.party_type = "Customer"
+		pr.party_type = (
+			self.party_type if hasattr(self, "party_type") and self.party_type else "Customer"
+		)
 		pr.party = self.customer
 		pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
 		pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
@@ -890,6 +895,42 @@
 		self.assertEqual(pr.allocation[0].allocated_amount, 85)
 		self.assertEqual(pr.allocation[0].difference_amount, 0)
 
+	def test_reconciliation_purchase_invoice_against_return(self):
+		pi = make_purchase_invoice(
+			supplier="_Test Supplier USD", currency="USD", conversion_rate=50
+		).submit()
+
+		pi_return = frappe.get_doc(pi.as_dict())
+		pi_return.name = None
+		pi_return.docstatus = 0
+		pi_return.is_return = 1
+		pi_return.conversion_rate = 80
+		pi_return.items[0].qty = -pi_return.items[0].qty
+		pi_return.submit()
+
+		self.company = "_Test Company"
+		self.party_type = "Supplier"
+		self.customer = "_Test Supplier USD"
+
+		pr = self.create_payment_reconciliation()
+		pr.get_unreconciled_entries()
+
+		invoices = []
+		payments = []
+		for invoice in pr.invoices:
+			if invoice.invoice_number == pi.name:
+				invoices.append(invoice.as_dict())
+				break
+		for payment in pr.payments:
+			if payment.reference_name == pi_return.name:
+				payments.append(payment.as_dict())
+				break
+
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+		# Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
+		pr.reconcile()
+
 
 def make_customer(customer_name, currency=None):
 	if not frappe.db.exists("Customer", customer_name):