refactor: assert payment ledger outstanding in both currencies
diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py
index 28a569b..fc30c4b 100644
--- a/erpnext/controllers/tests/test_accounts_controller.py
+++ b/erpnext/controllers/tests/test_accounts_controller.py
@@ -5,6 +5,7 @@
 
 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
 
@@ -48,7 +49,15 @@
 # class TestAccountsController(FrappeTestCase):
 class TestAccountsController(unittest.TestCase):
 	"""
-	Test Exchange Gain/Loss booking on various scenarios
+	Test Exchange Gain/Loss booking on various scenarios.
+	Test Cases are numbered for better readbility
+
+	10 series - Sales Invoice against Payment Entries
+	20 series - Sales Invoice against Journals
+	30 series - Sales Invoice against Credit Notes
+	40 series - Purchase Invoice against Payment Entries
+	50 series - Purchase Invoice against Journals
+	60 series - Purchase Invoice against Debit Notes
 	"""
 
 	def setUp(self):
@@ -130,7 +139,13 @@
 		self.debtors_usd = acc.name
 
 	def create_sales_invoice(
-		self, qty=1, rate=1, posting_date=nowdate(), do_not_save=False, do_not_submit=False
+		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
@@ -148,7 +163,7 @@
 			parent_cost_center=self.cost_center,
 			update_stock=0,
 			currency="USD",
-			conversion_rate=80,
+			conversion_rate=conversion_rate,
 			is_pos=0,
 			is_return=0,
 			return_against=None,
@@ -238,96 +253,140 @@
 			)
 		return journals
 
-	def test_01_payment_against_invoice(self):
-		# Sales Invoice in Foreign Currency
-		si = self.create_sales_invoice(qty=1, rate=1)
-		# Payment
-		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},
+	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]
 		)
-		pe = pe.save().submit()
+		self.assertEqual(outstanding, current_outstanding.outstanding)
+		self.assertEqual(
+			outstanding_in_account_currency, current_outstanding.outstanding_in_account_currency
+		)
 
-		si.reload()
-		self.assertEqual(si.outstanding_amount, 0)
+	def test_10_payment_against_sales_invoice(self):
+		# Sales Invoice in Foreign Currency
+		rate = 80
+		rate_in_account_currency = 1
 
-		# 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)
+		si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency)
 
-		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])
+		# 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()
 
-		# Cancel Payment
-		pe.cancel()
+				# 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)
 
-		si.reload()
-		self.assertEqual(si.outstanding_amount, 1)
+				# 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])
 
-		# 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)
+				# Cancel Payment
+				pe.cancel()
 
-		self.assertEqual(exc_je_for_si, [])
-		self.assertEqual(exc_je_for_pe, [])
+				# 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)
 
-	def test_02_advance_against_invoice(self):
+				# 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()
 
-		# Invoice in Foreign Currency
-		si = self.create_sales_invoice(qty=1, 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()
+		# 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()
 
-		adv.reload()
-		self.assertEqual(si.outstanding_amount, 0)
+				# 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)
+				# 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)
 
-		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()
 
-		# 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, [])
 
-		# 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_03_partial_advance_and_payment_for_invoice(self):
+	def test_12_partial_advance_and_payment_for_sales_invoice(self):
 		"""
-		Invoice with partial advance payment, and a normal payment
+		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()
 
-		# Invoice in Foreign Currency linked with advance
-		si = self.create_sales_invoice(qty=2, rate=1, do_not_submit=True)
+		# 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",
 			{
@@ -343,19 +402,20 @@
 		si = si.save()
 		si = si.submit()
 
+		# Outstanding should be there in both currencies
 		si.reload()
-		self.assertEqual(si.outstanding_amount, 1)
+		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
+		# Payment for remaining amount
 		pe = self.create_payment_entry(amount=1, source_exc_rate=75).save()
 		pe.append(
 			"references",
@@ -363,13 +423,14 @@
 		)
 		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)
@@ -384,21 +445,20 @@
 		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_04_partial_advance_and_payment_for_invoice_with_cancellation(self):
+	def test_13_partial_advance_and_payment_for_invoice_with_cancellation(self):
 		"""
-		Invoice with partial advance payment, and a normal payment. Cancel advance and payment.
+		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 in Foreign Currency linked with advance
-		si = self.create_sales_invoice(qty=2, rate=1, do_not_submit=True)
+		# invoice with advance(partial amount)
+		si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1, do_not_submit=True)
 		si.append(
 			"advances",
 			{
@@ -414,19 +474,20 @@
 		si = si.save()
 		si = si.submit()
 
+		# Outstanding should be there in both currencies
 		si.reload()
-		self.assertEqual(si.outstanding_amount, 1)
+		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
+		# Payment(remaining amount)
 		pe = self.create_payment_entry(amount=1, source_exc_rate=75).save()
 		pe.append(
 			"references",
@@ -434,13 +495,14 @@
 		)
 		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)
@@ -450,21 +512,22 @@
 		adv.reload()
 		adv.cancel()
 
+		# Outstanding should be there in both currencies, since advance is cancelled.
 		si.reload()
-		self.assertEqual(si.outstanding_amount, 1)
+		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_05_same_payment_split_against_invoice(self):
+	def test_14_same_payment_split_against_invoice(self):
 		# Invoice in Foreign Currency
-		si = self.create_sales_invoice(qty=2, rate=1)
+		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(
@@ -473,13 +536,14 @@
 		)
 		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)
@@ -491,32 +555,35 @@
 		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)
-
-		# Test exact payment allocation
 		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)