Merge pull request #35644 from ruthra-kumar/book_gain_loss_in_je

refactor: booking exchange gain/loss amount through journal
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index f07a4fa..7af40c4 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -58,6 +58,13 @@
 			validate_balance_type(self.account, adv_adj)
 			validate_frozen_account(self.account, adv_adj)
 
+			if (
+				self.voucher_type == "Journal Entry"
+				and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type")
+				== "Exchange Gain Or Loss"
+			):
+				return
+
 			if frappe.get_cached_value("Account", self.account, "account_type") not in [
 				"Receivable",
 				"Payable",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index ea4a2d4..1e1b3ba 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -18,6 +18,7 @@
 )
 from erpnext.accounts.party import get_party_account
 from erpnext.accounts.utils import (
+	cancel_exchange_gain_loss_journal,
 	get_account_currency,
 	get_balance_on,
 	get_stock_accounts,
@@ -87,9 +88,8 @@
 		self.update_invoice_discounting()
 
 	def on_cancel(self):
-		from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
-
-		unlink_ref_doc_from_payment_entries(self)
+		# References for this Journal are removed on the `on_cancel` event in accounts_controller
+		super(JournalEntry, self).on_cancel()
 		self.ignore_linked_doctypes = (
 			"GL Entry",
 			"Stock Ledger Entry",
@@ -499,11 +499,12 @@
 				)
 
 				if not against_entries:
-					frappe.throw(
-						_(
-							"Journal Entry {0} does not have account {1} or already matched against other voucher"
-						).format(d.reference_name, d.account)
-					)
+					if self.voucher_type != "Exchange Gain Or Loss":
+						frappe.throw(
+							_(
+								"Journal Entry {0} does not have account {1} or already matched against other voucher"
+							).format(d.reference_name, d.account)
+						)
 				else:
 					dr_or_cr = "debit" if d.credit > 0 else "credit"
 					valid = False
@@ -586,7 +587,9 @@
 						else:
 							party_account = against_voucher[1]
 
-					if against_voucher[0] != cstr(d.party) or party_account != d.account:
+					if (
+						against_voucher[0] != cstr(d.party) or party_account != d.account
+					) and self.voucher_type != "Exchange Gain Or Loss":
 						frappe.throw(
 							_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format(
 								d.idx,
@@ -768,18 +771,23 @@
 				)
 			):
 
-				# Modified to include the posting date for which to retreive the exchange rate
-				d.exchange_rate = get_exchange_rate(
-					self.posting_date,
-					d.account,
-					d.account_currency,
-					self.company,
-					d.reference_type,
-					d.reference_name,
-					d.debit,
-					d.credit,
-					d.exchange_rate,
-				)
+				ignore_exchange_rate = False
+				if self.get("flags") and self.flags.get("ignore_exchange_rate"):
+					ignore_exchange_rate = True
+
+				if not ignore_exchange_rate:
+					# Modified to include the posting date for which to retreive the exchange rate
+					d.exchange_rate = get_exchange_rate(
+						self.posting_date,
+						d.account,
+						d.account_currency,
+						self.company,
+						d.reference_type,
+						d.reference_name,
+						d.debit,
+						d.credit,
+						d.exchange_rate,
+					)
 
 			if not d.exchange_rate:
 				frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx))
@@ -935,6 +943,8 @@
 				merge_entries=merge_entries,
 				update_outstanding=update_outstanding,
 			)
+			if cancel:
+				cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
 
 	@frappe.whitelist()
 	def get_balance(self, difference_account=None):
diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
index e7aca79..a6e920b 100644
--- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
@@ -5,6 +5,7 @@
 import unittest
 
 import frappe
+from frappe.tests.utils import change_settings
 from frappe.utils import flt, nowdate
 
 from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -13,6 +14,7 @@
 
 
 class TestJournalEntry(unittest.TestCase):
+	@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
 	def test_journal_entry_with_against_jv(self):
 		jv_invoice = frappe.copy_doc(test_records[2])
 		base_jv = frappe.copy_doc(test_records[0])
diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
index 47ad19e..3ba8cea 100644
--- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
+++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
@@ -203,7 +203,7 @@
    "fieldtype": "Select",
    "label": "Reference Type",
    "no_copy": 1,
-   "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement"
+   "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry"
   },
   {
    "fieldname": "reference_name",
@@ -284,7 +284,7 @@
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-10-26 20:03:10.906259",
+ "modified": "2023-06-16 14:11:13.507807",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Journal Entry Account",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 44474d9..33f2634 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -9,7 +9,7 @@
 
 frappe.ui.form.on('Payment Entry', {
 	onload: function(frm) {
-		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"];
+		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Journal Entry", "Repost Payment Ledger"];
 
 		if(frm.doc.__islocal) {
 			if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index c3018cd..69ce19c 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -28,7 +28,12 @@
 	process_gl_map,
 )
 from erpnext.accounts.party import get_party_account
-from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices
+from erpnext.accounts.utils import (
+	cancel_exchange_gain_loss_journal,
+	get_account_currency,
+	get_balance_on,
+	get_outstanding_invoices,
+)
 from erpnext.controllers.accounts_controller import (
 	AccountsController,
 	get_supplier_block_status,
@@ -143,6 +148,7 @@
 			"Repost Payment Ledger",
 			"Repost Payment Ledger Items",
 		)
+		super(PaymentEntry, self).on_cancel()
 		self.make_gl_entries(cancel=1)
 		self.make_advance_gl_entries(cancel=1)
 		self.update_outstanding_amounts()
@@ -404,7 +410,7 @@
 			else:
 				if ref_doc:
 					if self.paid_from_account_currency == ref_doc.currency:
-						self.source_exchange_rate = ref_doc.get("exchange_rate")
+						self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
 
 			if not self.source_exchange_rate:
 				self.source_exchange_rate = get_exchange_rate(
@@ -417,7 +423,7 @@
 		elif self.paid_to and not self.target_exchange_rate:
 			if ref_doc:
 				if self.paid_to_account_currency == ref_doc.currency:
-					self.target_exchange_rate = ref_doc.get("exchange_rate")
+					self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
 
 			if not self.target_exchange_rate:
 				self.target_exchange_rate = get_exchange_rate(
@@ -813,10 +819,25 @@
 				flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
 			)
 		else:
+
+			# Use source/target exchange rate, so no difference amount is calculated.
+			# then update exchange gain/loss amount in reference table
+			# if there is an exchange gain/loss amount in reference table, submit a JE for that
+
+			exchange_rate = 1
+			if self.payment_type == "Receive":
+				exchange_rate = self.source_exchange_rate
+			elif self.payment_type == "Pay":
+				exchange_rate = self.target_exchange_rate
+
 			base_allocated_amount += flt(
-				flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
+				flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
 			)
 
+			allocated_amount_in_pe_exchange_rate = flt(
+				flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
+			)
+			d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate
 		return base_allocated_amount
 
 	def set_total_allocated_amount(self):
@@ -1007,6 +1028,10 @@
 		gl_entries = self.build_gl_map()
 		gl_entries = process_gl_map(gl_entries)
 		make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
+		if cancel:
+			cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
+		else:
+			self.make_exchange_gain_loss_journal()
 
 	def add_party_gl_entries(self, gl_entries):
 		if self.party_account:
@@ -1993,7 +2018,6 @@
 	payment_type=None,
 	reference_date=None,
 ):
-	reference_doc = None
 	doc = frappe.get_doc(dt, dn)
 	over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
 	if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (
@@ -2133,7 +2157,7 @@
 	update_accounting_dimensions(pe, doc)
 
 	if party_account and bank:
-		pe.set_exchange_rate(ref_doc=reference_doc)
+		pe.set_exchange_rate(ref_doc=doc)
 		pe.set_amounts()
 
 		if discount_amount:
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index dc44fc3..8f9f7ce 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -31,6 +31,16 @@
 	def tearDown(self):
 		frappe.db.rollback()
 
+	def get_journals_for(self, voucher_type: str, voucher_no: str) -> list:
+		journals = []
+		if voucher_type and voucher_no:
+			journals = frappe.db.get_all(
+				"Journal Entry Account",
+				filters={"reference_type": voucher_type, "reference_name": voucher_no, "docstatus": 1},
+				fields=["parent"],
+			)
+		return journals
+
 	def test_payment_entry_against_order(self):
 		so = make_sales_order()
 		pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
@@ -591,21 +601,15 @@
 		pe.target_exchange_rate = 45.263
 		pe.reference_no = "1"
 		pe.reference_date = "2016-01-01"
-
-		pe.append(
-			"deductions",
-			{
-				"account": "_Test Exchange Gain/Loss - _TC",
-				"cost_center": "_Test Cost Center - _TC",
-				"amount": 94.80,
-			},
-		)
-
 		pe.save()
 
 		self.assertEqual(flt(pe.difference_amount, 2), 0.0)
 		self.assertEqual(flt(pe.unallocated_amount, 2), 0.0)
 
+		# the exchange gain/loss amount is captured in reference table and a separate Journal will be submitted for them
+		# payment entry will not be generating difference amount
+		self.assertEqual(flt(pe.references[0].exchange_gain_loss, 2), -94.74)
+
 	def test_payment_entry_retrieves_last_exchange_rate(self):
 		from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
 			save_new_records,
@@ -792,33 +796,28 @@
 		pe.reference_no = "1"
 		pe.reference_date = "2016-01-01"
 		pe.source_exchange_rate = 55
-
-		pe.append(
-			"deductions",
-			{
-				"account": "_Test Exchange Gain/Loss - _TC",
-				"cost_center": "_Test Cost Center - _TC",
-				"amount": -500,
-			},
-		)
 		pe.save()
 
 		self.assertEqual(pe.unallocated_amount, 0)
 		self.assertEqual(pe.difference_amount, 0)
-
+		self.assertEqual(pe.references[0].exchange_gain_loss, 500)
 		pe.submit()
 
 		expected_gle = dict(
 			(d[0], d)
 			for d in [
-				["_Test Receivable USD - _TC", 0, 5000, si.name],
+				["_Test Receivable USD - _TC", 0, 5500, si.name],
 				["_Test Bank USD - _TC", 5500, 0, None],
-				["_Test Exchange Gain/Loss - _TC", 0, 500, None],
 			]
 		)
 
 		self.validate_gl_entries(pe.name, expected_gle)
 
+		# Exchange gain/loss should have been posted through a journal
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
+
+		self.assertEqual(exc_je_for_si, exc_je_for_pe)
 		outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
 		self.assertEqual(outstanding_amount, 0)
 
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 25d94c5..ea06e0e 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -14,6 +14,7 @@
 )
 from erpnext.accounts.utils import (
 	QueryPaymentLedger,
+	create_gain_loss_journal,
 	get_outstanding_invoices,
 	reconcile_against_document,
 )
@@ -276,6 +277,11 @@
 	def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
 		invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
 		invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
+		if payment_entry[0].get("reference_type") in ["Sales Invoice", "Purchase Invoice"]:
+			payment_entry[0]["exchange_rate"] = invoice_exchange_map.get(
+				payment_entry[0].get("reference_name")
+			)
+
 		new_difference_amount = self.get_difference_amount(
 			payment_entry[0], invoice[0], allocated_amount
 		)
@@ -363,12 +369,6 @@
 				payment_details = self.get_payment_details(row, dr_or_cr)
 				reconciled_entry.append(payment_details)
 
-				if payment_details.difference_amount and row.reference_type not in [
-					"Sales Invoice",
-					"Purchase Invoice",
-				]:
-					self.make_difference_entry(payment_details)
-
 		if entry_list:
 			reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
 
@@ -656,6 +656,7 @@
 						"reference_type": inv.against_voucher_type,
 						"reference_name": inv.against_voucher,
 						"cost_center": erpnext.get_default_cost_center(company),
+						"exchange_rate": inv.exchange_rate,
 					},
 					{
 						"account": inv.account,
@@ -669,13 +670,38 @@
 						"reference_type": inv.voucher_type,
 						"reference_name": inv.voucher_no,
 						"cost_center": erpnext.get_default_cost_center(company),
+						"exchange_rate": inv.exchange_rate,
 					},
 				],
 			}
 		)
 
-		if difference_entry := get_difference_row(inv):
-			jv.append("accounts", difference_entry)
-
 		jv.flags.ignore_mandatory = True
+		jv.flags.ignore_exchange_rate = True
 		jv.submit()
+
+		if inv.difference_amount != 0:
+			# make gain/loss journal
+			if inv.party_type == "Customer":
+				dr_or_cr = "credit" if inv.difference_amount < 0 else "debit"
+			else:
+				dr_or_cr = "debit" if inv.difference_amount < 0 else "credit"
+
+			reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
+
+			create_gain_loss_journal(
+				company,
+				inv.party_type,
+				inv.party,
+				inv.account,
+				inv.difference_account,
+				inv.difference_amount,
+				dr_or_cr,
+				reverse_dr_or_cr,
+				inv.voucher_type,
+				inv.voucher_no,
+				None,
+				inv.against_voucher_type,
+				inv.against_voucher,
+				None,
+			)
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 2ac7df0..1d843ab 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -686,14 +686,24 @@
 
 		# Check if difference journal entry gets generated for difference amount after reconciliation
 		pr.reconcile()
-		total_debit_amount = frappe.db.get_all(
+		total_credit_amount = frappe.db.get_all(
 			"Journal Entry Account",
 			{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
-			"sum(debit) as amount",
+			"sum(credit) as amount",
 			group_by="reference_name",
 		)[0].amount
 
-		self.assertEqual(flt(total_debit_amount, 2), -500)
+		# total credit includes the exchange gain/loss amount
+		self.assertEqual(flt(total_credit_amount, 2), 8500)
+
+		jea_parent = frappe.db.get_all(
+			"Journal Entry Account",
+			filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
+			fields=["parent"],
+		)[0]
+		self.assertEqual(
+			frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
+		)
 
 	def test_difference_amount_via_payment_entry(self):
 		# Make Sale Invoice
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index e17a846..feb2fdf 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -144,8 +144,7 @@
 			(d[0], d)
 			for d in [
 				["_Test Receivable USD - _TC", 0, 5000, si_usd.name],
-				[pr.payment_account, 6290.0, 0, None],
-				["_Test Exchange Gain/Loss - _TC", 0, 1290, None],
+				[pr.payment_account, 5000.0, 0, None],
 			]
 		)
 
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 96ba783..d175df5 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -536,6 +536,7 @@
 					merge_entries=False,
 					from_repost=from_repost,
 				)
+				self.make_exchange_gain_loss_journal()
 			elif self.docstatus == 2:
 				provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
 				make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -580,7 +581,6 @@
 			self.get_asset_gl_entry(gl_entries)
 
 		self.make_tax_gl_entries(gl_entries)
-		self.make_exchange_gain_loss_gl_entries(gl_entries)
 		self.make_internal_transfer_gl_entries(gl_entries)
 
 		gl_entries = make_regional_gl_entries(gl_entries, self)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 486e01e..55d0203 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1273,10 +1273,11 @@
 		pi.save()
 		pi.submit()
 
+		creditors_account = pi.credit_to
+
 		expected_gle = [
 			["_Test Account Cost for Goods Sold - _TC", 37500.0],
-			["_Test Payable USD - _TC", -35000.0],
-			["Exchange Gain/Loss - _TC", -2500.0],
+			["_Test Payable USD - _TC", -37500.0],
 		]
 
 		gl_entries = frappe.db.sql(
@@ -1293,6 +1294,31 @@
 			self.assertEqual(expected_gle[i][0], gle.account)
 			self.assertEqual(expected_gle[i][1], gle.balance)
 
+		pi.reload()
+		self.assertEqual(pi.outstanding_amount, 0)
+
+		total_debit_amount = frappe.db.get_all(
+			"Journal Entry Account",
+			{"account": creditors_account, "docstatus": 1, "reference_name": pi.name},
+			"sum(debit) as amount",
+			group_by="reference_name",
+		)[0].amount
+		self.assertEqual(flt(total_debit_amount, 2), 2500)
+		jea_parent = frappe.db.get_all(
+			"Journal Entry Account",
+			filters={
+				"account": creditors_account,
+				"docstatus": 1,
+				"reference_name": pi.name,
+				"debit": 2500,
+				"debit_in_account_currency": 0,
+			},
+			fields=["parent"],
+		)[0]
+		self.assertEqual(
+			frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
+		)
+
 		pi_2 = make_purchase_invoice(
 			supplier="_Test Supplier USD",
 			currency="USD",
@@ -1317,10 +1343,12 @@
 		pi_2.save()
 		pi_2.submit()
 
+		pi_2.reload()
+		self.assertEqual(pi_2.outstanding_amount, 0)
+
 		expected_gle = [
 			["_Test Account Cost for Goods Sold - _TC", 36500.0],
-			["_Test Payable USD - _TC", -35000.0],
-			["Exchange Gain/Loss - _TC", -1500.0],
+			["_Test Payable USD - _TC", -36500.0],
 		]
 
 		gl_entries = frappe.db.sql(
@@ -1351,12 +1379,39 @@
 			self.assertEqual(expected_gle[i][0], gle.account)
 			self.assertEqual(expected_gle[i][1], gle.balance)
 
+		total_debit_amount = frappe.db.get_all(
+			"Journal Entry Account",
+			{"account": creditors_account, "docstatus": 1, "reference_name": pi_2.name},
+			"sum(debit) as amount",
+			group_by="reference_name",
+		)[0].amount
+		self.assertEqual(flt(total_debit_amount, 2), 1500)
+		jea_parent_2 = frappe.db.get_all(
+			"Journal Entry Account",
+			filters={
+				"account": creditors_account,
+				"docstatus": 1,
+				"reference_name": pi_2.name,
+				"debit": 1500,
+				"debit_in_account_currency": 0,
+			},
+			fields=["parent"],
+		)[0]
+		self.assertEqual(
+			frappe.db.get_value("Journal Entry", jea_parent_2.parent, "voucher_type"),
+			"Exchange Gain Or Loss",
+		)
+
 		pi.reload()
 		pi.cancel()
 
+		self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent.parent, "docstatus"), 2)
+
 		pi_2.reload()
 		pi_2.cancel()
 
+		self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent_2.parent, "docstatus"), 2)
+
 		pay.reload()
 		pay.cancel()
 
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index b0cc8ca..f08bf18 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -23,7 +23,7 @@
 )
 from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
 from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
-from erpnext.accounts.utils import get_account_currency
+from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency
 from erpnext.assets.doctype.asset.depreciation import (
 	depreciate_asset,
 	get_disposal_account_and_cost_center,
@@ -1030,7 +1030,10 @@
 					merge_entries=False,
 					from_repost=from_repost,
 				)
+
+				self.make_exchange_gain_loss_journal()
 			elif self.docstatus == 2:
+				cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
 				make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
 
 			if update_outstanding == "No":
@@ -1055,7 +1058,6 @@
 		self.make_customer_gl_entry(gl_entries)
 
 		self.make_tax_gl_entries(gl_entries)
-		self.make_exchange_gain_loss_gl_entries(gl_entries)
 		self.make_internal_transfer_gl_entries(gl_entries)
 
 		self.make_item_gl_entries(gl_entries)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index e8445aa..8816a8c 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -3213,15 +3213,10 @@
 			account.disabled = 0
 			account.save()
 
+	@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
 	def test_gain_loss_with_advance_entry(self):
 		from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
 
-		unlink_enabled = frappe.db.get_value(
-			"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
-		)
-
-		frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1)
-
 		jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False)
 
 		jv.accounts[0].exchange_rate = 70
@@ -3254,18 +3249,28 @@
 		)
 		si.save()
 		si.submit()
-
 		expected_gle = [
-			["_Test Exchange Gain/Loss - _TC", 500.0, 0.0, nowdate()],
 			["_Test Receivable USD - _TC", 7500.0, 0.0, nowdate()],
-			["_Test Receivable USD - _TC", 0.0, 500.0, nowdate()],
 			["Sales - _TC", 0.0, 7500.0, nowdate()],
 		]
-
 		check_gl_entries(self, si.name, expected_gle, nowdate())
 
-		frappe.db.set_single_value(
-			"Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 0)
+		journals = frappe.db.get_all(
+			"Journal Entry Account",
+			filters={"reference_type": "Sales Invoice", "reference_name": si.name, "docstatus": 1},
+			pluck="parent",
+		)
+		journals = [x for x in journals if x != jv.name]
+		self.assertEqual(len(journals), 1)
+		je_type = frappe.get_cached_value("Journal Entry", journals[0], "voucher_type")
+		self.assertEqual(je_type, "Exchange Gain Or Loss")
+		ledger_outstanding = frappe.db.get_all(
+			"Payment Ledger Entry",
+			filters={"against_voucher_no": si.name, "delinked": 0},
+			fields=["sum(amount), sum(amount_in_account_currency)"],
+			as_list=1,
 		)
 
 	def test_batch_expiry_for_sales_invoice_return(self):
diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py
index 3aca60e..3cb5e42 100644
--- a/erpnext/accounts/test/test_utils.py
+++ b/erpnext/accounts/test/test_utils.py
@@ -80,18 +80,27 @@
 		item = make_item().name
 
 		purchase_invoice = make_purchase_invoice(
-			item=item, supplier="_Test Supplier USD", currency="USD", conversion_rate=82.32
+			item=item, supplier="_Test Supplier USD", currency="USD", conversion_rate=82.32, do_not_submit=1
 		)
+		purchase_invoice.credit_to = "_Test Payable USD - _TC"
 		purchase_invoice.submit()
 
 		payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name)
-		payment_entry.target_exchange_rate = 62.9
 		payment_entry.paid_amount = 15725
 		payment_entry.deductions = []
-		payment_entry.insert()
+		payment_entry.save()
 
-		self.assertEqual(payment_entry.difference_amount, -4855.00)
+		# below is the difference between base_received_amount and base_paid_amount
+		self.assertEqual(payment_entry.difference_amount, -4855.0)
+
+		payment_entry.target_exchange_rate = 62.9
+		payment_entry.save()
+
+		# below is due to change in exchange rate
+		self.assertEqual(payment_entry.references[0].exchange_gain_loss, -4855.0)
+
 		payment_entry.references = []
+		self.assertEqual(payment_entry.difference_amount, 0.0)
 		payment_entry.submit()
 
 		payment_reconciliation = frappe.new_doc("Payment Reconciliation")
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index ae0a279..961f41c 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -459,6 +459,9 @@
 			# update ref in advance entry
 			if voucher_type == "Journal Entry":
 				update_reference_in_journal_entry(entry, doc, do_not_save=True)
+				# advance section in sales/purchase invoice and reconciliation tool,both pass on exchange gain/loss
+				# amount and account in args
+				doc.make_exchange_gain_loss_journal(args)
 			else:
 				update_reference_in_payment_entry(
 					entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
@@ -618,9 +621,7 @@
 		"total_amount": d.grand_total,
 		"outstanding_amount": d.outstanding_amount,
 		"allocated_amount": d.allocated_amount,
-		"exchange_rate": d.exchange_rate
-		if not d.exchange_gain_loss
-		else payment_entry.get_exchange_rate(),
+		"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
 		"exchange_gain_loss": d.exchange_gain_loss,  # only populated from invoice in case of advance allocation
 		"account": d.account,
 	}
@@ -642,28 +643,48 @@
 		new_row.docstatus = 1
 		new_row.update(reference_details)
 
-	if d.difference_amount and d.difference_account:
-		account_details = {
-			"account": d.difference_account,
-			"cost_center": payment_entry.cost_center
-			or frappe.get_cached_value("Company", payment_entry.company, "cost_center"),
-		}
-		if d.difference_amount:
-			account_details["amount"] = d.difference_amount
-
-		payment_entry.set_gain_or_loss(account_details=account_details)
-
 	payment_entry.flags.ignore_validate_update_after_submit = True
 	payment_entry.setup_party_account_field()
 	payment_entry.set_missing_values()
 	if not skip_ref_details_update_for_pe:
 		payment_entry.set_missing_ref_details()
 	payment_entry.set_amounts()
+	payment_entry.make_exchange_gain_loss_journal()
 
 	if not do_not_save:
 		payment_entry.save(ignore_permissions=True)
 
 
+def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None:
+	"""
+	Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
+	"""
+	if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
+		journals = frappe.db.get_all(
+			"Journal Entry Account",
+			filters={
+				"reference_type": parent_doc.doctype,
+				"reference_name": parent_doc.name,
+				"docstatus": 1,
+			},
+			fields=["parent"],
+			as_list=1,
+		)
+
+		if journals:
+			gain_loss_journals = frappe.db.get_all(
+				"Journal Entry",
+				filters={
+					"name": ["in", [x[0] for x in journals]],
+					"voucher_type": "Exchange Gain Or Loss",
+					"docstatus": 1,
+				},
+				as_list=1,
+			)
+			for doc in gain_loss_journals:
+				frappe.get_doc("Journal Entry", doc[0]).cancel()
+
+
 def unlink_ref_doc_from_payment_entries(ref_doc):
 	remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name)
 	remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name)
@@ -1820,3 +1841,74 @@
 		self.query_for_outstanding()
 
 		return self.voucher_outstandings
+
+
+def create_gain_loss_journal(
+	company,
+	party_type,
+	party,
+	party_account,
+	gain_loss_account,
+	exc_gain_loss,
+	dr_or_cr,
+	reverse_dr_or_cr,
+	ref1_dt,
+	ref1_dn,
+	ref1_detail_no,
+	ref2_dt,
+	ref2_dn,
+	ref2_detail_no,
+) -> str:
+	journal_entry = frappe.new_doc("Journal Entry")
+	journal_entry.voucher_type = "Exchange Gain Or Loss"
+	journal_entry.company = company
+	journal_entry.posting_date = nowdate()
+	journal_entry.multi_currency = 1
+
+	party_account_currency = frappe.get_cached_value("Account", party_account, "account_currency")
+
+	if not gain_loss_account:
+		frappe.throw(_("Please set default Exchange Gain/Loss Account in Company {}").format(company))
+	gain_loss_account_currency = get_account_currency(gain_loss_account)
+	company_currency = frappe.get_cached_value("Company", company, "default_currency")
+
+	if gain_loss_account_currency != company_currency:
+		frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, company_currency))
+
+	journal_account = frappe._dict(
+		{
+			"account": party_account,
+			"party_type": party_type,
+			"party": party,
+			"account_currency": party_account_currency,
+			"exchange_rate": 0,
+			"cost_center": erpnext.get_default_cost_center(company),
+			"reference_type": ref1_dt,
+			"reference_name": ref1_dn,
+			"reference_detail_no": ref1_detail_no,
+			dr_or_cr: abs(exc_gain_loss),
+			dr_or_cr + "_in_account_currency": 0,
+		}
+	)
+
+	journal_entry.append("accounts", journal_account)
+
+	journal_account = frappe._dict(
+		{
+			"account": gain_loss_account,
+			"account_currency": gain_loss_account_currency,
+			"exchange_rate": 1,
+			"cost_center": erpnext.get_default_cost_center(company),
+			"reference_type": ref2_dt,
+			"reference_name": ref2_dn,
+			"reference_detail_no": ref2_detail_no,
+			reverse_dr_or_cr + "_in_account_currency": 0,
+			reverse_dr_or_cr: abs(exc_gain_loss),
+		}
+	)
+
+	journal_entry.append("accounts", journal_account)
+
+	journal_entry.save()
+	journal_entry.submit()
+	return journal_entry.name
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 7940489..37a18d8 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -5,7 +5,7 @@
 import json
 
 import frappe
-from frappe import _, bold, throw
+from frappe import _, bold, qb, throw
 from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
 from frappe.query_builder.custom import ConstantColumn
 from frappe.query_builder.functions import Abs, Sum
@@ -38,7 +38,12 @@
 	get_party_gle_currency,
 	validate_party_frozen_disabled,
 )
-from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
+from erpnext.accounts.utils import (
+	create_gain_loss_journal,
+	get_account_currency,
+	get_fiscal_years,
+	validate_fiscal_year,
+)
 from erpnext.buying.utils import update_last_purchase_rate
 from erpnext.controllers.print_settings import (
 	set_print_templates_for_item_table,
@@ -968,67 +973,133 @@
 
 				d.exchange_gain_loss = difference
 
-	def make_exchange_gain_loss_gl_entries(self, gl_entries):
-		if self.get("doctype") in ["Purchase Invoice", "Sales Invoice"]:
-			for d in self.get("advances"):
-				if d.exchange_gain_loss:
-					is_purchase_invoice = self.get("doctype") == "Purchase Invoice"
-					party = self.supplier if is_purchase_invoice else self.customer
-					party_account = self.credit_to if is_purchase_invoice else self.debit_to
-					party_type = "Supplier" if is_purchase_invoice else "Customer"
+	def make_exchange_gain_loss_journal(self, args: dict = None) -> None:
+		"""
+		Make Exchange Gain/Loss journal for Invoices and Payments
+		"""
+		# Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
+		# see accounts/utils.py:cancel_exchange_gain_loss_journal()
+		if self.docstatus == 1:
+			if self.get("doctype") == "Journal Entry":
+				# 'args' is populated with exchange gain/loss account and the amount to be booked.
+				# These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.
+				# and below logic is only for such scenarios
+				if args:
+					for arg in args:
+						# Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount`
+						if (
+							arg.get("difference_amount", 0) != 0 or arg.get("exchange_gain_loss", 0) != 0
+						) and arg.get("difference_account"):
 
-					gain_loss_account = frappe.get_cached_value(
-						"Company", self.company, "exchange_gain_loss_account"
-					)
-					if not gain_loss_account:
-						frappe.throw(
-							_("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company"))
-						)
-					account_currency = get_account_currency(gain_loss_account)
-					if account_currency != self.company_currency:
-						frappe.throw(
-							_("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency)
-						)
+							party_account = arg.get("account")
+							gain_loss_account = arg.get("difference_account")
+							difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss")
+							if difference_amount > 0:
+								dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit"
+							else:
+								dr_or_cr = "credit" if arg.get("party_type") == "Customer" else "debit"
 
-					# for purchase
-					dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
-					if not is_purchase_invoice:
-						# just reverse for sales?
-						dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
+							reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
 
-					gl_entries.append(
-						self.get_gl_dict(
-							{
-								"account": gain_loss_account,
-								"account_currency": account_currency,
-								"against": party,
-								dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
-								dr_or_cr: abs(d.exchange_gain_loss),
-								"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
-								"project": self.project,
-							},
-							item=d,
+							je = create_gain_loss_journal(
+								self.company,
+								arg.get("party_type"),
+								arg.get("party"),
+								party_account,
+								gain_loss_account,
+								difference_amount,
+								dr_or_cr,
+								reverse_dr_or_cr,
+								arg.get("against_voucher_type"),
+								arg.get("against_voucher"),
+								arg.get("idx"),
+								self.doctype,
+								self.name,
+								arg.get("idx"),
+							)
+							frappe.msgprint(
+								_("Exchange Gain/Loss amount has been booked through {0}").format(
+									get_link_to_form("Journal Entry", je)
+								)
+							)
+
+			if self.get("doctype") == "Payment Entry":
+				# For Payment Entry, exchange_gain_loss field in the `references` table is the trigger for journal creation
+				gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0]
+				booked = []
+				if gain_loss_to_book:
+					vtypes = [x.reference_doctype for x in gain_loss_to_book]
+					vnames = [x.reference_name for x in gain_loss_to_book]
+					je = qb.DocType("Journal Entry")
+					jea = qb.DocType("Journal Entry Account")
+					parents = (
+						qb.from_(jea)
+						.select(jea.parent)
+						.where(
+							(jea.reference_type == "Payment Entry")
+							& (jea.reference_name == self.name)
+							& (jea.docstatus == 1)
 						)
+						.run()
 					)
 
-					dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
-
-					gl_entries.append(
-						self.get_gl_dict(
-							{
-								"account": party_account,
-								"party_type": party_type,
-								"party": party,
-								"against": gain_loss_account,
-								dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate),
-								dr_or_cr: abs(d.exchange_gain_loss),
-								"cost_center": self.cost_center,
-								"project": self.project,
-							},
-							self.party_account_currency,
-							item=self,
+					booked = []
+					if parents:
+						booked = (
+							qb.from_(je)
+							.inner_join(jea)
+							.on(je.name == jea.parent)
+							.select(jea.reference_type, jea.reference_name, jea.reference_detail_no)
+							.where(
+								(je.docstatus == 1)
+								& (je.name.isin(parents))
+								& (je.voucher_type == "Exchange Gain or Loss")
+							)
+							.run()
 						)
-					)
+
+				for d in gain_loss_to_book:
+					# Filter out References for which Gain/Loss is already booked
+					if d.exchange_gain_loss and (
+						(d.reference_doctype, d.reference_name, str(d.idx)) not in booked
+					):
+						if self.payment_type == "Receive":
+							party_account = self.paid_from
+						elif self.payment_type == "Pay":
+							party_account = self.paid_to
+
+						dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
+
+						if d.reference_doctype == "Purchase Invoice":
+							dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
+
+						reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
+
+						gain_loss_account = frappe.get_cached_value(
+							"Company", self.company, "exchange_gain_loss_account"
+						)
+
+						je = create_gain_loss_journal(
+							self.company,
+							self.party_type,
+							self.party,
+							party_account,
+							gain_loss_account,
+							d.exchange_gain_loss,
+							dr_or_cr,
+							reverse_dr_or_cr,
+							d.reference_doctype,
+							d.reference_name,
+							d.idx,
+							self.doctype,
+							self.name,
+							d.idx,
+						)
+						frappe.msgprint(
+							_("Exchange Gain/Loss amount has been booked through {0}").format(
+								get_link_to_form("Journal Entry", je)
+							)
+						)
 
 	def update_against_document_in_jv(self):
 		"""
@@ -1090,9 +1161,15 @@
 			reconcile_against_document(lst)
 
 	def on_cancel(self):
-		from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
+		from erpnext.accounts.utils import (
+			cancel_exchange_gain_loss_journal,
+			unlink_ref_doc_from_payment_entries,
+		)
 
-		if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
+		if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
+			# Cancel Exchange Gain/Loss Journal before unlinking
+			cancel_exchange_gain_loss_journal(self)
+
 			if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"):
 				unlink_ref_doc_from_payment_entries(self)
 
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index caf4b6f..d669abe 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -15,7 +15,7 @@
 	make_reverse_gl_entries,
 	process_gl_map,
 )
-from erpnext.accounts.utils import get_fiscal_year
+from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
 from erpnext.controllers.accounts_controller import AccountsController
 from erpnext.stock import get_warehouse_account_map
 from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
@@ -534,6 +534,7 @@
 		make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
 
 	def make_gl_entries_on_cancel(self):
+		cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
 		if frappe.db.sql(
 			"""select name from `tabGL Entry` where voucher_type=%s
 			and voucher_no=%s""",
diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py
new file mode 100644
index 0000000..8e5f813
--- /dev/null
+++ b/erpnext/controllers/tests/test_accounts_controller.py
@@ -0,0 +1,980 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import unittest
+
+import frappe
+from frappe import qb
+from frappe.query_builder.functions import Sum
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import add_days, flt, nowdate
+
+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
+
+
+def make_customer(customer_name, currency=None):
+	if not frappe.db.exists("Customer", customer_name):
+		customer = frappe.new_doc("Customer")
+		customer.customer_name = customer_name
+		customer.customer_type = "Individual"
+
+		if currency:
+			customer.default_currency = currency
+		customer.save()
+		return customer.name
+	else:
+		return customer_name
+
+
+def make_supplier(supplier_name, currency=None):
+	if not frappe.db.exists("Supplier", supplier_name):
+		supplier = frappe.new_doc("Supplier")
+		supplier.supplier_name = supplier_name
+		supplier.supplier_type = "Individual"
+		supplier.supplier_group = "All Supplier Groups"
+
+		if currency:
+			supplier.default_currency = currency
+		supplier.save()
+		return supplier.name
+	else:
+		return supplier_name
+
+
+class TestAccountsController(FrappeTestCase):
+	"""
+	Test Exchange Gain/Loss booking on various scenarios.
+	Test Cases are numbered for better organization
+
+	10 series - Sales Invoice against Payment Entries
+	20 series - Sales Invoice against Journals
+	30 series - Sales Invoice against Credit Notes
+	"""
+
+	def setUp(self):
+		self.create_company()
+		self.create_account()
+		self.create_item()
+		self.create_parties()
+		self.clear_old_entries()
+
+	def tearDown(self):
+		frappe.db.rollback()
+
+	def create_company(self):
+		company_name = "_Test Company"
+		self.company_abbr = abbr = "_TC"
+		if frappe.db.exists("Company", company_name):
+			company = frappe.get_doc("Company", company_name)
+		else:
+			company = frappe.get_doc(
+				{
+					"doctype": "Company",
+					"company_name": company_name,
+					"country": "India",
+					"default_currency": "INR",
+					"create_chart_of_accounts_based_on": "Standard Template",
+					"chart_of_accounts": "Standard",
+				}
+			)
+			company = company.save()
+
+		self.company = company.name
+		self.cost_center = company.cost_center
+		self.warehouse = "Stores - " + abbr
+		self.finished_warehouse = "Finished Goods - " + abbr
+		self.income_account = "Sales - " + abbr
+		self.expense_account = "Cost of Goods Sold - " + abbr
+		self.debit_to = "Debtors - " + abbr
+		self.debit_usd = "Debtors USD - " + abbr
+		self.cash = "Cash - " + abbr
+		self.creditors = "Creditors - " + abbr
+
+	def create_item(self):
+		item = create_item(
+			item_code="_Test Notebook", is_stock_item=0, company=self.company, warehouse=self.warehouse
+		)
+		self.item = item if isinstance(item, str) else item.item_code
+
+	def create_parties(self):
+		self.create_customer()
+		self.create_supplier()
+
+	def create_customer(self):
+		self.customer = make_customer("_Test MC Customer USD", "USD")
+
+	def create_supplier(self):
+		self.supplier = make_supplier("_Test MC Supplier USD", "USD")
+
+	def create_account(self):
+		account_name = "Debtors USD"
+		if not frappe.db.get_value(
+			"Account", filters={"account_name": account_name, "company": self.company}
+		):
+			acc = frappe.new_doc("Account")
+			acc.account_name = account_name
+			acc.parent_account = "Accounts Receivable - " + self.company_abbr
+			acc.company = self.company
+			acc.account_currency = "USD"
+			acc.account_type = "Receivable"
+			acc.insert()
+		else:
+			name = frappe.db.get_value(
+				"Account",
+				filters={"account_name": account_name, "company": self.company},
+				fieldname="name",
+				pluck=True,
+			)
+			acc = frappe.get_doc("Account", name)
+		self.debtors_usd = acc.name
+
+	def create_sales_invoice(
+		self,
+		qty=1,
+		rate=1,
+		conversion_rate=80,
+		posting_date=nowdate(),
+		do_not_save=False,
+		do_not_submit=False,
+	):
+		"""
+		Helper function to populate default values in sales invoice
+		"""
+		sinv = create_sales_invoice(
+			qty=qty,
+			rate=rate,
+			company=self.company,
+			customer=self.customer,
+			item_code=self.item,
+			item_name=self.item,
+			cost_center=self.cost_center,
+			warehouse=self.warehouse,
+			debit_to=self.debit_usd,
+			parent_cost_center=self.cost_center,
+			update_stock=0,
+			currency="USD",
+			conversion_rate=conversion_rate,
+			is_pos=0,
+			is_return=0,
+			return_against=None,
+			income_account=self.income_account,
+			expense_account=self.expense_account,
+			do_not_save=do_not_save,
+			do_not_submit=do_not_submit,
+		)
+		return sinv
+
+	def create_payment_entry(
+		self, amount=1, source_exc_rate=75, posting_date=nowdate(), customer=None
+	):
+		"""
+		Helper function to populate default values in payment entry
+		"""
+		payment = create_payment_entry(
+			company=self.company,
+			payment_type="Receive",
+			party_type="Customer",
+			party=customer or self.customer,
+			paid_from=self.debit_usd,
+			paid_to=self.cash,
+			paid_amount=amount,
+		)
+		payment.source_exchange_rate = source_exc_rate
+		payment.received_amount = source_exc_rate * amount
+		payment.posting_date = posting_date
+		return payment
+
+	def clear_old_entries(self):
+		doctype_list = [
+			"GL Entry",
+			"Payment Ledger Entry",
+			"Sales Invoice",
+			"Purchase Invoice",
+			"Payment Entry",
+			"Journal Entry",
+		]
+		for doctype in doctype_list:
+			qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
+
+	def create_payment_reconciliation(self):
+		pr = frappe.new_doc("Payment Reconciliation")
+		pr.company = self.company
+		pr.party_type = "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()
+		return pr
+
+	def create_journal_entry(
+		self,
+		acc1=None,
+		acc1_exc_rate=None,
+		acc2_exc_rate=None,
+		acc2=None,
+		acc1_amount=0,
+		acc2_amount=0,
+		posting_date=None,
+		cost_center=None,
+	):
+		je = frappe.new_doc("Journal Entry")
+		je.posting_date = posting_date or nowdate()
+		je.company = self.company
+		je.user_remark = "test"
+		je.multi_currency = True
+		if not cost_center:
+			cost_center = self.cost_center
+		je.set(
+			"accounts",
+			[
+				{
+					"account": acc1,
+					"exchange_rate": acc1_exc_rate or 1,
+					"cost_center": cost_center,
+					"debit_in_account_currency": acc1_amount if acc1_amount > 0 else 0,
+					"credit_in_account_currency": abs(acc1_amount) if acc1_amount < 0 else 0,
+					"debit": acc1_amount * acc1_exc_rate if acc1_amount > 0 else 0,
+					"credit": abs(acc1_amount * acc1_exc_rate) if acc1_amount < 0 else 0,
+				},
+				{
+					"account": acc2,
+					"exchange_rate": acc2_exc_rate or 1,
+					"cost_center": cost_center,
+					"credit_in_account_currency": acc2_amount if acc2_amount > 0 else 0,
+					"debit_in_account_currency": abs(acc2_amount) if acc2_amount < 0 else 0,
+					"credit": acc2_amount * acc2_exc_rate if acc2_amount > 0 else 0,
+					"debit": abs(acc2_amount * acc2_exc_rate) if acc2_amount < 0 else 0,
+				},
+			],
+		)
+		return je
+
+	def get_journals_for(self, voucher_type: str, voucher_no: str) -> list:
+		journals = []
+		if voucher_type and voucher_no:
+			journals = frappe.db.get_all(
+				"Journal Entry Account",
+				filters={"reference_type": voucher_type, "reference_name": voucher_no, "docstatus": 1},
+				fields=["parent"],
+			)
+		return journals
+
+	def assert_ledger_outstanding(
+		self,
+		voucher_type: str,
+		voucher_no: str,
+		outstanding: float,
+		outstanding_in_account_currency: float,
+	) -> None:
+		"""
+		Assert outstanding amount based on ledger on both company/base currency and account currency
+		"""
+
+		ple = qb.DocType("Payment Ledger Entry")
+		current_outstanding = (
+			qb.from_(ple)
+			.select(
+				Sum(ple.amount).as_("outstanding"),
+				Sum(ple.amount_in_account_currency).as_("outstanding_in_account_currency"),
+			)
+			.where(
+				(ple.against_voucher_type == voucher_type)
+				& (ple.against_voucher_no == voucher_no)
+				& (ple.delinked == 0)
+			)
+			.run(as_dict=True)[0]
+		)
+		self.assertEqual(outstanding, current_outstanding.outstanding)
+		self.assertEqual(
+			outstanding_in_account_currency, current_outstanding.outstanding_in_account_currency
+		)
+
+	def test_10_payment_against_sales_invoice(self):
+		# Sales Invoice in Foreign Currency
+		rate = 80
+		rate_in_account_currency = 1
+
+		si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency)
+
+		# Test payments with different exchange rates
+		for exc_rate in [75.9, 83.1, 80.01]:
+			with self.subTest(exc_rate=exc_rate):
+				pe = self.create_payment_entry(amount=1, source_exc_rate=exc_rate).save()
+				pe.append(
+					"references",
+					{"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
+				)
+				pe = pe.save().submit()
+
+				# Outstanding in both currencies should be '0'
+				si.reload()
+				self.assertEqual(si.outstanding_amount, 0)
+				self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+				# Exchange Gain/Loss Journal should've been created.
+				exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+				exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
+				self.assertNotEqual(exc_je_for_si, [])
+				self.assertEqual(len(exc_je_for_si), 1)
+				self.assertEqual(len(exc_je_for_pe), 1)
+				self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
+
+				# Cancel Payment
+				pe.cancel()
+
+				# outstanding should be same as grand total
+				si.reload()
+				self.assertEqual(si.outstanding_amount, rate_in_account_currency)
+				self.assert_ledger_outstanding(si.doctype, si.name, rate, rate_in_account_currency)
+
+				# Exchange Gain/Loss Journal should've been cancelled
+				exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+				exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
+				self.assertEqual(exc_je_for_si, [])
+				self.assertEqual(exc_je_for_pe, [])
+
+	def test_11_advance_against_sales_invoice(self):
+		# Advance Payment
+		adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit()
+		adv.reload()
+
+		# Sales Invoices in different exchange rates
+		for exc_rate in [75.9, 83.1, 80.01]:
+			with self.subTest(exc_rate=exc_rate):
+				si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True)
+				si.append(
+					"advances",
+					{
+						"doctype": "Sales Invoice Advance",
+						"reference_type": adv.doctype,
+						"reference_name": adv.name,
+						"advance_amount": 1,
+						"allocated_amount": 1,
+						"ref_exchange_rate": 85,
+						"remarks": "Test",
+					},
+				)
+				si = si.save()
+				si = si.submit()
+
+				# Outstanding in both currencies should be '0'
+				adv.reload()
+				self.assertEqual(si.outstanding_amount, 0)
+				self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+				# Exchange Gain/Loss Journal should've been created.
+				exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+				exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+				self.assertNotEqual(exc_je_for_si, [])
+				self.assertEqual(len(exc_je_for_si), 1)
+				self.assertEqual(len(exc_je_for_adv), 1)
+				self.assertEqual(exc_je_for_si, exc_je_for_adv)
+
+				# Cancel Invoice
+				si.cancel()
+
+				# Exchange Gain/Loss Journal should've been cancelled
+				exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+				exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+				self.assertEqual(exc_je_for_si, [])
+				self.assertEqual(exc_je_for_adv, [])
+
+	def test_12_partial_advance_and_payment_for_sales_invoice(self):
+		"""
+		Sales invoice with partial advance payment, and a normal payment reconciled
+		"""
+		# Partial Advance
+		adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit()
+		adv.reload()
+
+		# sales invoice with advance(partial amount)
+		rate = 80
+		rate_in_account_currency = 1
+		si = self.create_sales_invoice(
+			qty=2, conversion_rate=80, rate=rate_in_account_currency, do_not_submit=True
+		)
+		si.append(
+			"advances",
+			{
+				"doctype": "Sales Invoice Advance",
+				"reference_type": adv.doctype,
+				"reference_name": adv.name,
+				"advance_amount": 1,
+				"allocated_amount": 1,
+				"ref_exchange_rate": 85,
+				"remarks": "Test",
+			},
+		)
+		si = si.save()
+		si = si.submit()
+
+		# Outstanding should be there in both currencies
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 1)  # account currency
+		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+		# Exchange Gain/Loss Journal should've been created for the partial advance
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+		self.assertNotEqual(exc_je_for_si, [])
+		self.assertEqual(len(exc_je_for_si), 1)
+		self.assertEqual(len(exc_je_for_adv), 1)
+		self.assertEqual(exc_je_for_si, exc_je_for_adv)
+
+		# Payment for remaining amount
+		pe = self.create_payment_entry(amount=1, source_exc_rate=75).save()
+		pe.append(
+			"references",
+			{"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
+		)
+		pe = pe.save().submit()
+
+		# Outstanding in both currencies should be '0'
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 0)
+		self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+		# Exchange Gain/Loss Journal should've been created for the payment
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
+		self.assertNotEqual(exc_je_for_si, [])
+		# There should be 2 JE's now. One for the advance and one for the payment
+		self.assertEqual(len(exc_je_for_si), 2)
+		self.assertEqual(len(exc_je_for_pe), 1)
+		self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv)
+
+		# Cancel Invoice
+		si.reload()
+		si.cancel()
+
+		# Exchange Gain/Loss Journal should been cancelled
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
+		exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+		self.assertEqual(exc_je_for_si, [])
+		self.assertEqual(exc_je_for_pe, [])
+		self.assertEqual(exc_je_for_adv, [])
+
+	def test_13_partial_advance_and_payment_for_invoice_with_cancellation(self):
+		"""
+		Invoice with partial advance payment, and a normal payment. Then cancel advance and payment.
+		"""
+		# Partial Advance
+		adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit()
+		adv.reload()
+
+		# invoice with advance(partial amount)
+		si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1, do_not_submit=True)
+		si.append(
+			"advances",
+			{
+				"doctype": "Sales Invoice Advance",
+				"reference_type": adv.doctype,
+				"reference_name": adv.name,
+				"advance_amount": 1,
+				"allocated_amount": 1,
+				"ref_exchange_rate": 85,
+				"remarks": "Test",
+			},
+		)
+		si = si.save()
+		si = si.submit()
+
+		# Outstanding should be there in both currencies
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 1)  # account currency
+		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+		# Exchange Gain/Loss Journal should've been created for the partial advance
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+		self.assertNotEqual(exc_je_for_si, [])
+		self.assertEqual(len(exc_je_for_si), 1)
+		self.assertEqual(len(exc_je_for_adv), 1)
+		self.assertEqual(exc_je_for_si, exc_je_for_adv)
+
+		# Payment(remaining amount)
+		pe = self.create_payment_entry(amount=1, source_exc_rate=75).save()
+		pe.append(
+			"references",
+			{"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
+		)
+		pe = pe.save().submit()
+
+		# Outstanding should be '0' in both currencies
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 0)
+		self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+		# Exchange Gain/Loss Journal should've been created for the payment
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
+		self.assertNotEqual(exc_je_for_si, [])
+		# There should be 2 JE's now. One for the advance and one for the payment
+		self.assertEqual(len(exc_je_for_si), 2)
+		self.assertEqual(len(exc_je_for_pe), 1)
+		self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv)
+
+		adv.reload()
+		adv.cancel()
+
+		# Outstanding should be there in both currencies, since advance is cancelled.
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 1)  # account currency
+		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
+		exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+		# Exchange Gain/Loss Journal for advance should been cancelled
+		self.assertEqual(len(exc_je_for_si), 1)
+		self.assertEqual(len(exc_je_for_pe), 1)
+		self.assertEqual(exc_je_for_adv, [])
+
+	def test_14_same_payment_split_against_invoice(self):
+		# Invoice in Foreign Currency
+		si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1)
+		# Payment
+		pe = self.create_payment_entry(amount=2, source_exc_rate=75).save()
+		pe.append(
+			"references",
+			{"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
+		)
+		pe = pe.save().submit()
+
+		# There should be outstanding in both currencies
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 1)
+		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+		# Exchange Gain/Loss Journal should've been created.
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
+		self.assertNotEqual(exc_je_for_si, [])
+		self.assertEqual(len(exc_je_for_si), 1)
+		self.assertEqual(len(exc_je_for_pe), 1)
+		self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
+
+		# Reconcile the remaining amount
+		pr = frappe.get_doc("Payment Reconciliation")
+		pr.company = self.company
+		pr.party_type = "Customer"
+		pr.party = self.customer
+		pr.receivable_payable_account = self.debit_usd
+		pr.get_unreconciled_entries()
+		self.assertEqual(len(pr.invoices), 1)
+		self.assertEqual(len(pr.payments), 1)
+		invoices = [x.as_dict() for x in pr.invoices]
+		payments = [x.as_dict() for x in pr.payments]
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+		pr.reconcile()
+		self.assertEqual(len(pr.invoices), 0)
+		self.assertEqual(len(pr.payments), 0)
+
+		# Exc gain/loss journal should have been creaetd for the reconciled amount
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
+		self.assertEqual(len(exc_je_for_si), 2)
+		self.assertEqual(len(exc_je_for_pe), 2)
+		self.assertEqual(exc_je_for_si, exc_je_for_pe)
+
+		# There should be no outstanding
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 0)
+		self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+		# Cancel Payment
+		pe.reload()
+		pe.cancel()
+
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 2)
+		self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
+
+		# Exchange Gain/Loss Journal should've been cancelled
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
+		self.assertEqual(exc_je_for_si, [])
+		self.assertEqual(exc_je_for_pe, [])
+
+	def test_20_journal_against_sales_invoice(self):
+		# Invoice in Foreign Currency
+		si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
+		# Payment
+		je = self.create_journal_entry(
+			acc1=self.debit_usd,
+			acc1_exc_rate=75,
+			acc2=self.cash,
+			acc1_amount=-1,
+			acc2_amount=-75,
+			acc2_exc_rate=1,
+		)
+		je.accounts[0].party_type = "Customer"
+		je.accounts[0].party = self.customer
+		je = je.save().submit()
+
+		# Reconcile the remaining amount
+		pr = self.create_payment_reconciliation()
+		# pr.receivable_payable_account = self.debit_usd
+		pr.get_unreconciled_entries()
+		self.assertEqual(len(pr.invoices), 1)
+		self.assertEqual(len(pr.payments), 1)
+		invoices = [x.as_dict() for x in pr.invoices]
+		payments = [x.as_dict() for x in pr.payments]
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+		pr.reconcile()
+		self.assertEqual(len(pr.invoices), 0)
+		self.assertEqual(len(pr.payments), 0)
+
+		# There should be no outstanding in both currencies
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 0)
+		self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+		# Exchange Gain/Loss Journal should've been created.
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_je = self.get_journals_for(je.doctype, je.name)
+		self.assertNotEqual(exc_je_for_si, [])
+		self.assertEqual(
+			len(exc_je_for_si), 2
+		)  # payment also has reference. so, there are 2 journals referencing invoice
+		self.assertEqual(len(exc_je_for_je), 1)
+		self.assertIn(exc_je_for_je[0], exc_je_for_si)
+
+		# Cancel Payment
+		je.reload()
+		je.cancel()
+
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 1)
+		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+		# Exchange Gain/Loss Journal should've been cancelled
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_je = self.get_journals_for(je.doctype, je.name)
+		self.assertEqual(exc_je_for_si, [])
+		self.assertEqual(exc_je_for_je, [])
+
+	def test_21_advance_journal_against_sales_invoice(self):
+		# Advance Payment
+		adv_exc_rate = 80
+		adv = self.create_journal_entry(
+			acc1=self.debit_usd,
+			acc1_exc_rate=adv_exc_rate,
+			acc2=self.cash,
+			acc1_amount=-1,
+			acc2_amount=adv_exc_rate * -1,
+			acc2_exc_rate=1,
+		)
+		adv.accounts[0].party_type = "Customer"
+		adv.accounts[0].party = self.customer
+		adv.accounts[0].is_advance = "Yes"
+		adv = adv.save().submit()
+		adv.reload()
+
+		# Sales Invoices in different exchange rates
+		for exc_rate in [75.9, 83.1, 80.01]:
+			with self.subTest(exc_rate=exc_rate):
+				si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True)
+				si.append(
+					"advances",
+					{
+						"doctype": "Sales Invoice Advance",
+						"reference_type": adv.doctype,
+						"reference_name": adv.name,
+						"reference_row": adv.accounts[0].name,
+						"advance_amount": 1,
+						"allocated_amount": 1,
+						"ref_exchange_rate": adv_exc_rate,
+						"remarks": "Test",
+					},
+				)
+				si = si.save()
+				si = si.submit()
+
+				# Outstanding in both currencies should be '0'
+				adv.reload()
+				self.assertEqual(si.outstanding_amount, 0)
+				self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+				# Exchange Gain/Loss Journal should've been created.
+				exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name]
+				exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+				self.assertNotEqual(exc_je_for_si, [])
+				self.assertEqual(len(exc_je_for_si), 1)
+				self.assertEqual(len(exc_je_for_adv), 1)
+				self.assertEqual(exc_je_for_si, exc_je_for_adv)
+
+				# Cancel Invoice
+				si.cancel()
+
+				# Exchange Gain/Loss Journal should've been cancelled
+				exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+				exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+				self.assertEqual(exc_je_for_si, [])
+				self.assertEqual(exc_je_for_adv, [])
+
+	def test_22_partial_advance_and_payment_for_invoice_with_cancellation(self):
+		"""
+		Invoice with partial advance payment as Journal, and a normal payment. Then cancel advance and payment.
+		"""
+		# Partial Advance
+		adv_exc_rate = 75
+		adv = self.create_journal_entry(
+			acc1=self.debit_usd,
+			acc1_exc_rate=adv_exc_rate,
+			acc2=self.cash,
+			acc1_amount=-1,
+			acc2_amount=adv_exc_rate * -1,
+			acc2_exc_rate=1,
+		)
+		adv.accounts[0].party_type = "Customer"
+		adv.accounts[0].party = self.customer
+		adv.accounts[0].is_advance = "Yes"
+		adv = adv.save().submit()
+		adv.reload()
+
+		# invoice with advance(partial amount)
+		si = self.create_sales_invoice(qty=3, conversion_rate=80, rate=1, do_not_submit=True)
+		si.append(
+			"advances",
+			{
+				"doctype": "Sales Invoice Advance",
+				"reference_type": adv.doctype,
+				"reference_name": adv.name,
+				"reference_row": adv.accounts[0].name,
+				"advance_amount": 1,
+				"allocated_amount": 1,
+				"ref_exchange_rate": adv_exc_rate,
+				"remarks": "Test",
+			},
+		)
+		si = si.save()
+		si = si.submit()
+
+		# Outstanding should be there in both currencies
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 2)  # account currency
+		self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
+
+		# Exchange Gain/Loss Journal should've been created for the partial advance
+		exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name]
+		exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+		self.assertNotEqual(exc_je_for_si, [])
+		self.assertEqual(len(exc_je_for_si), 1)
+		self.assertEqual(len(exc_je_for_adv), 1)
+		self.assertEqual(exc_je_for_si, exc_je_for_adv)
+
+		# Payment
+		adv2_exc_rate = 83
+		pay = self.create_journal_entry(
+			acc1=self.debit_usd,
+			acc1_exc_rate=adv2_exc_rate,
+			acc2=self.cash,
+			acc1_amount=-2,
+			acc2_amount=adv2_exc_rate * -2,
+			acc2_exc_rate=1,
+		)
+		pay.accounts[0].party_type = "Customer"
+		pay.accounts[0].party = self.customer
+		pay.accounts[0].is_advance = "Yes"
+		pay = pay.save().submit()
+		pay.reload()
+
+		# Reconcile the remaining amount
+		pr = self.create_payment_reconciliation()
+		# pr.receivable_payable_account = self.debit_usd
+		pr.get_unreconciled_entries()
+		self.assertEqual(len(pr.invoices), 1)
+		self.assertEqual(len(pr.payments), 1)
+		invoices = [x.as_dict() for x in pr.invoices]
+		payments = [x.as_dict() for x in pr.payments]
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+		pr.reconcile()
+		self.assertEqual(len(pr.invoices), 0)
+		self.assertEqual(len(pr.payments), 0)
+
+		# Outstanding should be '0' in both currencies
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 0)
+		self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+		# Exchange Gain/Loss Journal should've been created for the payment
+		exc_je_for_si = [
+			x
+			for x in self.get_journals_for(si.doctype, si.name)
+			if x.parent != adv.name and x.parent != pay.name
+		]
+		exc_je_for_pe = self.get_journals_for(pay.doctype, pay.name)
+		self.assertNotEqual(exc_je_for_si, [])
+		# There should be 2 JE's now. One for the advance and one for the payment
+		self.assertEqual(len(exc_je_for_si), 2)
+		self.assertEqual(len(exc_je_for_pe), 1)
+		self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv)
+
+		adv.reload()
+		adv.cancel()
+
+		# Outstanding should be there in both currencies, since advance is cancelled.
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 1)  # account currency
+		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+		exc_je_for_si = [
+			x
+			for x in self.get_journals_for(si.doctype, si.name)
+			if x.parent != adv.name and x.parent != pay.name
+		]
+		exc_je_for_pe = self.get_journals_for(pay.doctype, pay.name)
+		exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
+		# Exchange Gain/Loss Journal for advance should been cancelled
+		self.assertEqual(len(exc_je_for_si), 1)
+		self.assertEqual(len(exc_je_for_pe), 1)
+		self.assertEqual(exc_je_for_adv, [])
+
+	def test_23_same_journal_split_against_single_invoice(self):
+		# Invoice in Foreign Currency
+		si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1)
+		# Payment
+		je = self.create_journal_entry(
+			acc1=self.debit_usd,
+			acc1_exc_rate=75,
+			acc2=self.cash,
+			acc1_amount=-2,
+			acc2_amount=-150,
+			acc2_exc_rate=1,
+		)
+		je.accounts[0].party_type = "Customer"
+		je.accounts[0].party = self.customer
+		je = je.save().submit()
+
+		# Reconcile the first half
+		pr = self.create_payment_reconciliation()
+		pr.get_unreconciled_entries()
+		self.assertEqual(len(pr.invoices), 1)
+		self.assertEqual(len(pr.payments), 1)
+		invoices = [x.as_dict() for x in pr.invoices]
+		payments = [x.as_dict() for x in pr.payments]
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+		difference_amount = pr.calculate_difference_on_allocation_change(
+			[x.as_dict() for x in pr.payments], [x.as_dict() for x in pr.invoices], 1
+		)
+		pr.allocation[0].allocated_amount = 1
+		pr.allocation[0].difference_amount = difference_amount
+		pr.reconcile()
+		self.assertEqual(len(pr.invoices), 1)
+		self.assertEqual(len(pr.payments), 1)
+
+		# There should be outstanding in both currencies
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 1)
+		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+		# Exchange Gain/Loss Journal should've been created.
+		exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name]
+		exc_je_for_je = self.get_journals_for(je.doctype, je.name)
+		self.assertNotEqual(exc_je_for_si, [])
+		self.assertEqual(len(exc_je_for_si), 1)
+		self.assertEqual(len(exc_je_for_je), 1)
+		self.assertIn(exc_je_for_je[0], exc_je_for_si)
+
+		# reconcile remaining half
+		pr.get_unreconciled_entries()
+		self.assertEqual(len(pr.invoices), 1)
+		self.assertEqual(len(pr.payments), 1)
+		invoices = [x.as_dict() for x in pr.invoices]
+		payments = [x.as_dict() for x in pr.payments]
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+		pr.allocation[0].allocated_amount = 1
+		pr.allocation[0].difference_amount = difference_amount
+		pr.reconcile()
+		self.assertEqual(len(pr.invoices), 0)
+		self.assertEqual(len(pr.payments), 0)
+
+		# Exchange Gain/Loss Journal should've been created.
+		exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name]
+		exc_je_for_je = self.get_journals_for(je.doctype, je.name)
+		self.assertNotEqual(exc_je_for_si, [])
+		self.assertEqual(len(exc_je_for_si), 2)
+		self.assertEqual(len(exc_je_for_je), 2)
+		self.assertIn(exc_je_for_je[0], exc_je_for_si)
+
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 0)
+		self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
+
+		# Cancel Payment
+		je.reload()
+		je.cancel()
+
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 2)
+		self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
+
+		# Exchange Gain/Loss Journal should've been cancelled
+		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
+		exc_je_for_je = self.get_journals_for(je.doctype, je.name)
+		self.assertEqual(exc_je_for_si, [])
+		self.assertEqual(exc_je_for_je, [])
+
+	def test_30_cr_note_against_sales_invoice(self):
+		"""
+		Reconciling Cr Note against Sales Invoice, both having different exchange rates
+		"""
+		# Invoice in Foreign currency
+		si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1)
+
+		# Cr Note in Foreign currency of different exchange rate
+		cr_note = self.create_sales_invoice(qty=-2, conversion_rate=75, rate=1, do_not_save=True)
+		cr_note.is_return = 1
+		cr_note.save().submit()
+
+		# Reconcile the first half
+		pr = self.create_payment_reconciliation()
+		pr.get_unreconciled_entries()
+		self.assertEqual(len(pr.invoices), 1)
+		self.assertEqual(len(pr.payments), 1)
+		invoices = [x.as_dict() for x in pr.invoices]
+		payments = [x.as_dict() for x in pr.payments]
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+		difference_amount = pr.calculate_difference_on_allocation_change(
+			[x.as_dict() for x in pr.payments], [x.as_dict() for x in pr.invoices], 1
+		)
+		pr.allocation[0].allocated_amount = 1
+		pr.allocation[0].difference_amount = difference_amount
+		pr.reconcile()
+		self.assertEqual(len(pr.invoices), 1)
+		self.assertEqual(len(pr.payments), 1)
+
+		# Exchange Gain/Loss Journal should've been created.
+		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), 2)
+		self.assertEqual(len(exc_je_for_cr), 2)
+		self.assertEqual(exc_je_for_cr, exc_je_for_si)
+
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 1)
+		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+
+		cr_note.reload()
+		cr_note.cancel()
+
+		# Exchange Gain/Loss Journal should've been created.
+		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(len(exc_je_for_cr), 0)
+
+		# The Credit Note JE is still active and is referencing the sales invoice
+		# So, outstanding stays the same
+		si.reload()
+		self.assertEqual(si.outstanding_amount, 1)
+		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 0f4238c..641d755 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -320,6 +320,7 @@
 erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
 erpnext.patches.v14_0.update_closing_balances #14-07-2023
 execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
+erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts
 # below migration patches should always run last
 erpnext.patches.v14_0.migrate_gl_to_payment_ledger
 execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
diff --git a/erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py b/erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py
new file mode 100644
index 0000000..48b6bcf
--- /dev/null
+++ b/erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py
@@ -0,0 +1,22 @@
+import frappe
+
+
+def execute():
+	"""
+	Update Propery Setters for Journal Entry with new 'Entry Type'
+	"""
+	new_reference_type = "Payment Entry"
+	prop_setter = frappe.db.get_list(
+		"Property Setter",
+		filters={
+			"doc_type": "Journal Entry Account",
+			"field_name": "reference_type",
+			"property": "options",
+		},
+	)
+	if prop_setter:
+		property_setter_doc = frappe.get_doc("Property Setter", prop_setter[0].get("name"))
+
+		if new_reference_type not in property_setter_doc.value.split("\n"):
+			property_setter_doc.value += "\n" + new_reference_type
+			property_setter_doc.save()