Merge branch 'develop' into FIX-ISS-22-23-06369
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 3f985b6..c0eed18 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -31,6 +31,7 @@
   "determine_address_tax_category_from",
   "column_break_19",
   "add_taxes_from_item_tax_template",
+  "book_tax_discount_loss",
   "print_settings",
   "show_inclusive_tax_in_print",
   "column_break_12",
@@ -360,6 +361,13 @@
    "fieldname": "show_balance_in_coa",
    "fieldtype": "Check",
    "label": "Show Balances in Chart Of Accounts"
+  },
+  {
+   "default": "0",
+   "description": "Split Early Payment Discount Loss into Income and Tax Loss",
+   "fieldname": "book_tax_discount_loss",
+   "fieldtype": "Check",
+   "label": "Book Tax Loss on Early Payment Discount"
   }
  ],
  "icon": "icon-cog",
@@ -367,7 +375,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2023-01-02 12:07:42.434214",
+ "modified": "2023-03-28 09:50:20.375233",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
index 80878ac..0817187 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -81,7 +81,7 @@
 
 		loan_disbursement = frappe.qb.DocType("Loan Disbursement")
 
-		loan_disbursements = (
+		query = (
 			frappe.qb.from_(loan_disbursement)
 			.select(
 				ConstantColumn("Loan Disbursement").as_("payment_document"),
@@ -90,17 +90,22 @@
 				ConstantColumn(0).as_("debit"),
 				loan_disbursement.reference_number.as_("cheque_number"),
 				loan_disbursement.reference_date.as_("cheque_date"),
+				loan_disbursement.clearance_date.as_("clearance_date"),
 				loan_disbursement.disbursement_date.as_("posting_date"),
 				loan_disbursement.applicant.as_("against_account"),
 			)
 			.where(loan_disbursement.docstatus == 1)
 			.where(loan_disbursement.disbursement_date >= self.from_date)
 			.where(loan_disbursement.disbursement_date <= self.to_date)
-			.where(loan_disbursement.clearance_date.isnull())
 			.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
 			.orderby(loan_disbursement.disbursement_date)
 			.orderby(loan_disbursement.name, order=frappe.qb.desc)
-		).run(as_dict=1)
+		)
+
+		if not self.include_reconciled_entries:
+			query = query.where(loan_disbursement.clearance_date.isnull())
+
+		loan_disbursements = query.run(as_dict=1)
 
 		loan_repayment = frappe.qb.DocType("Loan Repayment")
 
@@ -113,16 +118,19 @@
 				ConstantColumn(0).as_("credit"),
 				loan_repayment.reference_number.as_("cheque_number"),
 				loan_repayment.reference_date.as_("cheque_date"),
+				loan_repayment.clearance_date.as_("clearance_date"),
 				loan_repayment.applicant.as_("against_account"),
 				loan_repayment.posting_date,
 			)
 			.where(loan_repayment.docstatus == 1)
-			.where(loan_repayment.clearance_date.isnull())
 			.where(loan_repayment.posting_date >= self.from_date)
 			.where(loan_repayment.posting_date <= self.to_date)
 			.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
 		)
 
+		if not self.include_reconciled_entries:
+			query = query.where(loan_repayment.clearance_date.isnull())
+
 		if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
 			query = query.where((loan_repayment.repay_from_salary == 0))
 
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index cb7da17..d6e1be4 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -325,14 +325,14 @@
 
 	if template_type == "Blank Template":
 		for root_type in get_root_types():
-			writer.writerow(["", "", "", 1, "", root_type])
+			writer.writerow(["", "", "", "", 1, "", root_type])
 
 		for account in get_mandatory_group_accounts():
-			writer.writerow(["", "", "", 1, account, "Asset"])
+			writer.writerow(["", "", "", "", 1, account, "Asset"])
 
 		for account_type in get_mandatory_account_types():
 			writer.writerow(
-				["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
+				["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
 			)
 	else:
 		writer = get_sample_template(writer)
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 2a8e2527..f8969b8 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -244,8 +244,6 @@
 		frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
 			party_account_currency, "references");
 
-		frm.set_currency_labels(["amount"], company_currency, "deductions");
-
 		cur_frm.set_df_property("source_exchange_rate", "description",
 			("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));
 
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index cd5b6d5..c34bddd 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -416,7 +416,7 @@
 
 		for ref in self.get("references"):
 			if ref.payment_term and ref.reference_name:
-				key = (ref.payment_term, ref.reference_name)
+				key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
 				invoice_payment_amount_map.setdefault(key, 0.0)
 				invoice_payment_amount_map[key] += ref.allocated_amount
 
@@ -424,20 +424,37 @@
 					payment_schedule = frappe.get_all(
 						"Payment Schedule",
 						filters={"parent": ref.reference_name},
-						fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"],
+						fields=[
+							"paid_amount",
+							"payment_amount",
+							"payment_term",
+							"discount",
+							"outstanding",
+							"discount_type",
+						],
 					)
 					for term in payment_schedule:
-						invoice_key = (term.payment_term, ref.reference_name)
+						invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
 						invoice_paid_amount_map.setdefault(invoice_key, {})
 						invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
-						invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
-							term.discount / 100
-						)
+						if not (term.discount_type and term.discount):
+							continue
+
+						if term.discount_type == "Percentage":
+							invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
+								term.discount / 100
+							)
+						else:
+							invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount
 
 		for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
 			if not invoice_paid_amount_map.get(key):
 				frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
 
+			allocated_amount = self.get_allocated_amount_in_transaction_currency(
+				allocated_amount, key[2], key[1]
+			)
+
 			outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
 			discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
 
@@ -472,6 +489,33 @@
 						(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
 					)
 
+	def get_allocated_amount_in_transaction_currency(
+		self, allocated_amount, reference_doctype, reference_docname
+	):
+		"""
+		Payment Entry could be in base currency while reference's payment schedule
+		is always in transaction currency.
+		E.g.
+		* SI with base=INR and currency=USD
+		* SI with payment schedule in USD
+		* PE in INR (accounting done in base currency)
+		"""
+		ref_currency, ref_exchange_rate = frappe.db.get_value(
+			reference_doctype, reference_docname, ["currency", "conversion_rate"]
+		)
+		is_single_currency = self.paid_from_account_currency == self.paid_to_account_currency
+		# PE in different currency
+		reference_is_multi_currency = self.paid_from_account_currency != ref_currency
+
+		if not (is_single_currency and reference_is_multi_currency):
+			return allocated_amount
+
+		allocated_amount = flt(
+			allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")
+		)
+
+		return allocated_amount
+
 	def set_status(self):
 		if self.docstatus == 2:
 			self.status = "Cancelled"
@@ -1642,7 +1686,14 @@
 
 @frappe.whitelist()
 def get_payment_entry(
-	dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None
+	dt,
+	dn,
+	party_amount=None,
+	bank_account=None,
+	bank_amount=None,
+	party_type=None,
+	payment_type=None,
+	reference_date=None,
 ):
 	reference_doc = None
 	doc = frappe.get_doc(dt, dn)
@@ -1669,8 +1720,9 @@
 		dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
 	)
 
-	paid_amount, received_amount, discount_amount = apply_early_payment_discount(
-		paid_amount, received_amount, doc
+	reference_date = getdate(reference_date)
+	paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount(
+		paid_amount, received_amount, doc, party_account_currency, reference_date
 	)
 
 	pe = frappe.new_doc("Payment Entry")
@@ -1678,6 +1730,7 @@
 	pe.company = doc.company
 	pe.cost_center = doc.get("cost_center")
 	pe.posting_date = nowdate()
+	pe.reference_date = reference_date
 	pe.mode_of_payment = doc.get("mode_of_payment")
 	pe.party_type = party_type
 	pe.party = doc.get(scrub(party_type))
@@ -1718,7 +1771,7 @@
 		):
 
 			for reference in get_reference_as_per_payment_terms(
-				doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount
+				doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
 			):
 				pe.append("references", reference)
 		else:
@@ -1769,16 +1822,17 @@
 	if party_account and bank:
 		pe.set_exchange_rate(ref_doc=reference_doc)
 		pe.set_amounts()
+
 		if discount_amount:
-			pe.set_gain_or_loss(
-				account_details={
-					"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
-					"cost_center": pe.cost_center
-					or frappe.get_cached_value("Company", pe.company, "cost_center"),
-					"amount": discount_amount * (-1 if payment_type == "Pay" else 1),
-				}
+			base_total_discount_loss = 0
+			if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
+				base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts)
+
+			set_pending_discount_loss(
+				pe, doc, discount_amount, base_total_discount_loss, party_account_currency
 			)
-			pe.set_difference_amount()
+
+		pe.set_difference_amount()
 
 	return pe
 
@@ -1889,20 +1943,28 @@
 	return paid_amount, received_amount
 
 
-def apply_early_payment_discount(paid_amount, received_amount, doc):
+def apply_early_payment_discount(
+	paid_amount, received_amount, doc, party_account_currency, reference_date
+):
 	total_discount = 0
+	valid_discounts = []
 	eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
 	has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
+	is_multi_currency = party_account_currency != doc.company_currency
 
 	if doc.doctype in eligible_for_payments and has_payment_schedule:
 		for term in doc.payment_schedule:
-			if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
+			if not term.discounted_amount and term.discount and reference_date <= term.discount_date:
+
 				if term.discount_type == "Percentage":
-					discount_amount = flt(doc.get("grand_total")) * (term.discount / 100)
+					grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total")
+					discount_amount = flt(grand_total) * (term.discount / 100)
 				else:
 					discount_amount = term.discount
 
-				discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1)
+				# if accounting is done in the same currency, paid_amount = received_amount
+				conversion_rate = doc.get("conversion_rate", 1) if is_multi_currency else 1
+				discount_amount_in_foreign_currency = discount_amount * conversion_rate
 
 				if doc.doctype == "Sales Invoice":
 					paid_amount -= discount_amount
@@ -1911,23 +1973,151 @@
 					received_amount -= discount_amount
 					paid_amount -= discount_amount_in_foreign_currency
 
+				valid_discounts.append({"type": term.discount_type, "discount": term.discount})
 				total_discount += discount_amount
 
 		if total_discount:
-			money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency"))
+			currency = doc.get("currency") if is_multi_currency else doc.company_currency
+			money = frappe.utils.fmt_money(total_discount, currency=currency)
 			frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
 
-	return paid_amount, received_amount, total_discount
+	return paid_amount, received_amount, total_discount, valid_discounts
+
+
+def set_pending_discount_loss(
+	pe, doc, discount_amount, base_total_discount_loss, party_account_currency
+):
+	# If multi-currency, get base discount amount to adjust with base currency deductions/losses
+	if party_account_currency != doc.company_currency:
+		discount_amount = discount_amount * doc.get("conversion_rate", 1)
+
+	# Avoid considering miniscule losses
+	discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total"))
+
+	# Set base discount amount (discount loss/pending rounding loss) in deductions
+	if discount_amount > 0.0:
+		positive_negative = -1 if pe.payment_type == "Pay" else 1
+
+		# If tax loss booking is enabled, pending loss will be rounding loss.
+		# Otherwise it will be the total discount loss.
+		book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
+		account_type = "round_off_account" if book_tax_loss else "default_discount_account"
+
+		pe.set_gain_or_loss(
+			account_details={
+				"account": frappe.get_cached_value("Company", pe.company, account_type),
+				"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
+				"amount": discount_amount * positive_negative,
+			}
+		)
+
+
+def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float:
+	"""Split early payment discount into Income Loss & Tax Loss."""
+	total_discount_percent = get_total_discount_percent(doc, valid_discounts)
+
+	if not total_discount_percent:
+		return 0.0
+
+	base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent)
+	base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent)
+
+	# Round off total loss rather than individual losses to reduce rounding error
+	return flt(base_loss_on_income + base_loss_on_taxes, doc.precision("grand_total"))
+
+
+def get_total_discount_percent(doc, valid_discounts) -> float:
+	"""Get total percentage and amount discount applied as a percentage."""
+	total_discount_percent = (
+		sum(
+			discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage"
+		)
+		or 0.0
+	)
+
+	# Operate in percentages only as it makes the income & tax split easier
+	total_discount_amount = (
+		sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Amount")
+		or 0.0
+	)
+
+	if total_discount_amount:
+		discount_percentage = (total_discount_amount / doc.get("grand_total")) * 100
+		total_discount_percent += discount_percentage
+		return total_discount_percent
+
+	return total_discount_percent
+
+
+def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
+	"""Add loss on income discount in base currency."""
+	precision = doc.precision("total")
+	base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
+
+	pe.append(
+		"deductions",
+		{
+			"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
+			"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
+			"amount": flt(base_loss_on_income, precision),
+		},
+	)
+
+	return base_loss_on_income  # Return loss without rounding
+
+
+def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
+	"""Add loss on tax discount in base currency."""
+	tax_discount_loss = {}
+	base_total_tax_loss = 0
+	precision = doc.precision("tax_amount_after_discount_amount", "taxes")
+
+	# The same account head could be used more than once
+	for tax in doc.get("taxes", []):
+		base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (
+			total_discount_percentage / 100
+		)
+
+		account = tax.get("account_head")
+		if not tax_discount_loss.get(account):
+			tax_discount_loss[account] = base_tax_loss
+		else:
+			tax_discount_loss[account] += base_tax_loss
+
+	for account, loss in tax_discount_loss.items():
+		base_total_tax_loss += loss
+		if loss == 0.0:
+			continue
+
+		pe.append(
+			"deductions",
+			{
+				"account": account,
+				"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
+				"amount": flt(loss, precision),
+			},
+		)
+
+	return base_total_tax_loss  # Return loss without rounding
 
 
 def get_reference_as_per_payment_terms(
-	payment_schedule, dt, dn, doc, grand_total, outstanding_amount
+	payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
 ):
 	references = []
+	is_multi_currency_acc = (doc.currency != doc.company_currency) and (
+		party_account_currency != doc.company_currency
+	)
+
 	for payment_term in payment_schedule:
 		payment_term_outstanding = flt(
 			payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
 		)
+		if not is_multi_currency_acc:
+			# If accounting is done in company currency for multi-currency transaction
+			payment_term_outstanding = flt(
+				payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount")
+			)
 
 		if payment_term_outstanding:
 			references.append(
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 123b5df..67049c4 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -5,7 +5,7 @@
 
 import frappe
 from frappe import qb
-from frappe.tests.utils import FrappeTestCase
+from frappe.tests.utils import FrappeTestCase, change_settings
 from frappe.utils import flt, nowdate
 
 from erpnext.accounts.doctype.payment_entry.payment_entry import (
@@ -256,10 +256,25 @@
 			},
 		)
 		si.save()
-
 		si.submit()
 
+		frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
+		pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+
+		self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
+		self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0)
+		self.assertEqual(pe_with_tax_loss.paid_amount, 212.4)
+		self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0)  # Loss on Income
+		self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6)  # Loss on Tax
+		self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
+
+		frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
 		pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+
+		self.assertEqual(pe.references[0].allocated_amount, 236.0)
+		self.assertEqual(pe.paid_amount, 212.4)
+		self.assertEqual(pe.deductions[0].amount, 23.6)
+
 		pe.submit()
 		si.load_from_db()
 
@@ -269,6 +284,190 @@
 		self.assertEqual(si.payment_schedule[0].outstanding, 0)
 		self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
 
+	def test_payment_entry_against_payment_terms_with_discount_amount(self):
+		si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
+
+		si.payment_terms_template = "Test Discount Amount Template"
+		create_payment_terms_template_with_discount(
+			name="30 Credit Days with Rs.50 Discount",
+			discount_type="Amount",
+			discount=50,
+			template_name="Test Discount Amount Template",
+		)
+		frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
+
+		si.append(
+			"taxes",
+			{
+				"charge_type": "On Net Total",
+				"account_head": "_Test Account Service Tax - _TC",
+				"cost_center": "_Test Cost Center - _TC",
+				"description": "Service Tax",
+				"rate": 18,
+			},
+		)
+		si.save()
+		si.submit()
+
+		# Set reference date past discount cut off date
+		pe_1 = get_payment_entry(
+			"Sales Invoice",
+			si.name,
+			bank_account="_Test Cash - _TC",
+			reference_date=frappe.utils.add_days(si.posting_date, 2),
+		)
+		self.assertEqual(pe_1.paid_amount, 236.0)  # discount not applied
+
+		# Test if tax loss is booked on enabling configuration
+		frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
+		pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+		self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37)  # Loss on Income
+		self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63)  # Loss on Tax
+		self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
+
+		frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
+		pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+		self.assertEqual(pe.references[0].allocated_amount, 236.0)
+		self.assertEqual(pe.paid_amount, 186)
+		self.assertEqual(pe.deductions[0].amount, 50.0)
+
+		pe.submit()
+		si.load_from_db()
+
+		self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
+		self.assertEqual(si.payment_schedule[0].paid_amount, 186)
+		self.assertEqual(si.payment_schedule[0].outstanding, 0)
+		self.assertEqual(si.payment_schedule[0].discounted_amount, 50)
+
+	@change_settings(
+		"Accounts Settings",
+		{
+			"allow_multi_currency_invoices_against_single_party_account": 1,
+			"book_tax_discount_loss": 1,
+		},
+	)
+	def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount(
+		self,
+	):
+		"""
+		1. Multi-currency SI with single currency accounting (company currency)
+		2. PE with early payment discount
+		3. Test if Paid Amount is calculated in company currency
+		4. Test if deductions are calculated in company currency
+
+		SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency.
+		"""
+		si = create_sales_invoice(
+			customer="_Test Customer",
+			currency="USD",
+			conversion_rate=50,
+			do_not_save=1,
+		)
+		create_payment_terms_template_with_discount()
+		si.payment_terms_template = "Test Discount Template"
+
+		frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
+		si.save()
+		si.submit()
+
+		pe = get_payment_entry(
+			"Sales Invoice",
+			si.name,
+			bank_account="_Test Bank - _TC",
+		)
+		pe.reference_no = si.name
+		pe.reference_date = nowdate()
+
+		# Early payment discount loss on income
+		self.assertEqual(pe.paid_amount, 4500.0)  # Amount in company currency
+		self.assertEqual(pe.received_amount, 4500.0)
+		self.assertEqual(pe.deductions[0].amount, 500.0)
+		self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
+		self.assertEqual(pe.difference_amount, 0.0)
+
+		pe.insert()
+		pe.submit()
+
+		expected_gle = dict(
+			(d[0], d)
+			for d in [
+				["Debtors - _TC", 0, 5000, si.name],
+				["_Test Bank - _TC", 4500, 0, None],
+				["Write Off - _TC", 500.0, 0, None],
+			]
+		)
+
+		self.validate_gl_entries(pe.name, expected_gle)
+
+		outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
+		self.assertEqual(outstanding_amount, 0)
+
+	def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self):
+		"""
+		1. Multi-currency SI with multi-currency accounting
+		2. PE with early payment discount and also exchange loss
+		3. Test if Paid Amount is calculated in transaction currency
+		4. Test if deductions are calculated in base/company currency
+		5. Test if exchange loss is reflected in difference
+		"""
+		si = create_sales_invoice(
+			customer="_Test Customer USD",
+			debit_to="_Test Receivable USD - _TC",
+			currency="USD",
+			conversion_rate=50,
+			do_not_save=1,
+		)
+		create_payment_terms_template_with_discount()
+		si.payment_terms_template = "Test Discount Template"
+
+		frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
+		si.save()
+		si.submit()
+
+		pe = get_payment_entry(
+			"Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700
+		)
+		pe.reference_no = si.name
+		pe.reference_date = nowdate()
+
+		# Early payment discount loss on income
+		self.assertEqual(pe.paid_amount, 90.0)
+		self.assertEqual(pe.received_amount, 4200.0)  # 5000 - 500 (discount) - 300 (exchange loss)
+		self.assertEqual(pe.deductions[0].amount, 500.0)
+		self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
+
+		# Exchange loss
+		self.assertEqual(pe.difference_amount, 300.0)
+
+		pe.append(
+			"deductions",
+			{
+				"account": "_Test Exchange Gain/Loss - _TC",
+				"cost_center": "_Test Cost Center - _TC",
+				"amount": 300.0,
+			},
+		)
+
+		pe.insert()
+		pe.submit()
+
+		self.assertEqual(pe.difference_amount, 0.0)
+
+		expected_gle = dict(
+			(d[0], d)
+			for d in [
+				["_Test Receivable USD - _TC", 0, 5000, si.name],
+				["_Test Bank - _TC", 4200, 0, None],
+				["Write Off - _TC", 500.0, 0, None],
+				["_Test Exchange Gain/Loss - _TC", 300.0, 0, None],
+			]
+		)
+
+		self.validate_gl_entries(pe.name, expected_gle)
+
+		outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
+		self.assertEqual(outstanding_amount, 0)
+
 	def test_payment_against_purchase_invoice_to_check_status(self):
 		pi = make_purchase_invoice(
 			supplier="_Test Supplier USD",
@@ -839,24 +1038,27 @@
 		).insert()
 
 
-def create_payment_terms_template_with_discount():
+def create_payment_terms_template_with_discount(
+	name=None, discount_type=None, discount=None, template_name=None
+):
+	create_payment_term(name or "30 Credit Days with 10% Discount")
+	template_name = template_name or "Test Discount Template"
 
-	create_payment_term("30 Credit Days with 10% Discount")
-
-	if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
-		payment_term_template = frappe.get_doc(
+	if not frappe.db.exists("Payment Terms Template", template_name):
+		frappe.get_doc(
 			{
 				"doctype": "Payment Terms Template",
-				"template_name": "Test Discount Template",
+				"template_name": template_name,
 				"allocate_payment_based_on_payment_terms": 1,
 				"terms": [
 					{
 						"doctype": "Payment Terms Template Detail",
-						"payment_term": "30 Credit Days with 10% Discount",
+						"payment_term": name or "30 Credit Days with 10% Discount",
 						"invoice_portion": 100,
 						"credit_days_based_on": "Day(s) after invoice date",
 						"credit_days": 2,
-						"discount": 10,
+						"discount_type": discount_type or "Percentage",
+						"discount": discount or 10,
 						"discount_validity_based_on": "Day(s) after invoice date",
 						"discount_validity": 1,
 					}
diff --git a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
index 61a1462..1c31829 100644
--- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
+++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
@@ -3,6 +3,7 @@
  "creation": "2016-06-15 15:56:30.815503",
  "doctype": "DocType",
  "editable_grid": 1,
+ "engine": "InnoDB",
  "field_order": [
   "account",
   "cost_center",
@@ -17,9 +18,7 @@
    "in_list_view": 1,
    "label": "Account",
    "options": "Account",
-   "reqd": 1,
-   "show_days": 1,
-   "show_seconds": 1
+   "reqd": 1
   },
   {
    "fieldname": "cost_center",
@@ -28,37 +27,30 @@
    "label": "Cost Center",
    "options": "Cost Center",
    "print_hide": 1,
-   "reqd": 1,
-   "show_days": 1,
-   "show_seconds": 1
+   "reqd": 1
   },
   {
    "fieldname": "amount",
    "fieldtype": "Currency",
    "in_list_view": 1,
-   "label": "Amount",
-   "reqd": 1,
-   "show_days": 1,
-   "show_seconds": 1
+   "label": "Amount (Company Currency)",
+   "options": "Company:company:default_currency",
+   "reqd": 1
   },
   {
    "fieldname": "column_break_2",
-   "fieldtype": "Column Break",
-   "show_days": 1,
-   "show_seconds": 1
+   "fieldtype": "Column Break"
   },
   {
    "fieldname": "description",
    "fieldtype": "Small Text",
-   "label": "Description",
-   "show_days": 1,
-   "show_seconds": 1
+   "label": "Description"
   }
  ],
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2020-09-12 20:38:08.110674",
+ "modified": "2023-03-06 07:11:57.739619",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Payment Entry Deduction",
@@ -66,5 +58,6 @@
  "permissions": [],
  "quick_entry": 1,
  "sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index d986f32..caffac5 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -272,4 +272,32 @@
 	}
 };
 
+frappe.ui.form.on('Payment Reconciliation Allocation', {
+	allocated_amount: function(frm, cdt, cdn) {
+		let row = locals[cdt][cdn];
+		// filter invoice
+		let invoice = frm.doc.invoices.filter((x) => (x.invoice_number == row.invoice_number));
+		// filter payment
+		let payment = frm.doc.payments.filter((x) => (x.reference_name == row.reference_name));
+
+		frm.call({
+			doc: frm.doc,
+			method: 'calculate_difference_on_allocation_change',
+			args: {
+				payment_entry: payment,
+				invoice: invoice,
+				allocated_amount: row.allocated_amount
+			},
+			callback: (r) => {
+				if (r.message) {
+					row.difference_amount = r.message;
+					frm.refresh();
+				}
+			}
+		});
+	}
+});
+
+
+
 extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm}));
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index c9e3998..d8082d0 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -234,6 +234,15 @@
 		return difference_amount
 
 	@frappe.whitelist()
+	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"))
+		new_difference_amount = self.get_difference_amount(
+			payment_entry[0], invoice[0], allocated_amount
+		)
+		return new_difference_amount
+
+	@frappe.whitelist()
 	def allocate_entries(self, args):
 		self.validate_entries()
 
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index e2b4a1a..5c9168b 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -82,7 +82,11 @@
 
 		if(doc.docstatus == 1 && doc.outstanding_amount != 0
 			&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
-			this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create'));
+			this.frm.add_custom_button(
+				__('Payment'),
+				() => this.make_payment_entry(),
+				__('Create')
+			);
 			cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
 		}
 
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index b79af71..a617447 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -117,7 +117,7 @@
 		self.validate_expense_account()
 		self.set_against_expense_account()
 		self.validate_write_off_account()
-		self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
+		self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
 		self.create_remarks()
 		self.set_status()
 		self.validate_purchase_receipt_if_update_stock()
@@ -232,7 +232,7 @@
 		)
 
 		if (
-			cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate"))
+			cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))
 			and not self.is_return
 			and not self.is_internal_supplier
 		):
@@ -581,6 +581,7 @@
 
 		self.make_supplier_gl_entry(gl_entries)
 		self.make_item_gl_entries(gl_entries)
+		self.make_precision_loss_gl_entry(gl_entries)
 
 		if self.check_asset_cwip_enabled():
 			self.get_asset_gl_entry(gl_entries)
@@ -975,6 +976,28 @@
 							item.item_tax_amount, item.precision("item_tax_amount")
 						)
 
+	def make_precision_loss_gl_entry(self, gl_entries):
+		round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+			self.company, "Purchase Invoice", self.name
+		)
+
+		precision_loss = self.get("base_net_total") - flt(
+			self.get("net_total") * self.conversion_rate, self.precision("net_total")
+		)
+
+		if precision_loss:
+			gl_entries.append(
+				self.get_gl_dict(
+					{
+						"account": round_off_account,
+						"against": self.supplier,
+						"credit": precision_loss,
+						"cost_center": self.cost_center or round_off_cost_center,
+						"remarks": _("Net total calculation precision loss"),
+					}
+				)
+			)
+
 	def get_asset_gl_entry(self, gl_entries):
 		arbnb_account = self.get_company_default("asset_received_but_not_billed")
 		eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 47e3f9b..56e412b 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -93,9 +93,12 @@
 
 		if (doc.docstatus == 1 && doc.outstanding_amount!=0
 			&& !(cint(doc.is_return) && doc.return_against)) {
-			cur_frm.add_custom_button(__('Payment'),
-				this.make_payment_entry, __('Create'));
-			cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
+			this.frm.add_custom_button(
+				__('Payment'),
+				() => this.make_payment_entry(),
+				__('Create')
+			);
+			this.frm.page.set_inner_btn_group_as_primary(__('Create'));
 		}
 
 		if(doc.docstatus==1 && !doc.is_return) {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 5cda276..db61995 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -145,7 +145,7 @@
 
 		self.set_against_income_account()
 		self.validate_time_sheets_are_submitted()
-		self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
+		self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
 		if not self.is_return:
 			self.validate_serial_numbers()
 		else:
diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
index 449ebdc..306af72 100644
--- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
+++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
@@ -4,6 +4,7 @@
 
 import frappe
 from frappe import _
+from frappe.query_builder.custom import ConstantColumn
 from frappe.utils import getdate, nowdate
 
 
@@ -91,4 +92,65 @@
 		as_list=1,
 	)
 
-	return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate()))
+	# Loan Disbursement
+	loan_disbursement = frappe.qb.DocType("Loan Disbursement")
+
+	query = (
+		frappe.qb.from_(loan_disbursement)
+		.select(
+			ConstantColumn("Loan Disbursement").as_("payment_document_type"),
+			loan_disbursement.name.as_("payment_entry"),
+			loan_disbursement.disbursement_date.as_("posting_date"),
+			loan_disbursement.reference_number.as_("cheque_no"),
+			loan_disbursement.clearance_date.as_("clearance_date"),
+			loan_disbursement.applicant.as_("against"),
+			-loan_disbursement.disbursed_amount.as_("amount"),
+		)
+		.where(loan_disbursement.docstatus == 1)
+		.where(loan_disbursement.disbursement_date >= filters["from_date"])
+		.where(loan_disbursement.disbursement_date <= filters["to_date"])
+		.where(loan_disbursement.disbursement_account == filters["account"])
+		.orderby(loan_disbursement.disbursement_date, order=frappe.qb.desc)
+		.orderby(loan_disbursement.name, order=frappe.qb.desc)
+	)
+
+	if filters.get("from_date"):
+		query = query.where(loan_disbursement.disbursement_date >= filters["from_date"])
+	if filters.get("to_date"):
+		query = query.where(loan_disbursement.disbursement_date <= filters["to_date"])
+
+	loan_disbursements = query.run(as_list=1)
+
+	# Loan Repayment
+	loan_repayment = frappe.qb.DocType("Loan Repayment")
+
+	query = (
+		frappe.qb.from_(loan_repayment)
+		.select(
+			ConstantColumn("Loan Repayment").as_("payment_document_type"),
+			loan_repayment.name.as_("payment_entry"),
+			loan_repayment.posting_date.as_("posting_date"),
+			loan_repayment.reference_number.as_("cheque_no"),
+			loan_repayment.clearance_date.as_("clearance_date"),
+			loan_repayment.applicant.as_("against"),
+			loan_repayment.amount_paid.as_("amount"),
+		)
+		.where(loan_repayment.docstatus == 1)
+		.where(loan_repayment.posting_date >= filters["from_date"])
+		.where(loan_repayment.posting_date <= filters["to_date"])
+		.where(loan_repayment.payment_account == filters["account"])
+		.orderby(loan_repayment.posting_date, order=frappe.qb.desc)
+		.orderby(loan_repayment.name, order=frappe.qb.desc)
+	)
+
+	if filters.get("from_date"):
+		query = query.where(loan_repayment.posting_date >= filters["from_date"])
+	if filters.get("to_date"):
+		query = query.where(loan_repayment.posting_date <= filters["to_date"])
+
+	loan_repayments = query.run(as_list=1)
+
+	return sorted(
+		journal_entries + payment_entries + loan_disbursements + loan_repayments,
+		key=lambda k: k[2] or getdate(nowdate()),
+	)
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 47089f7..c6c9f1f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -236,7 +236,11 @@
 							this.make_purchase_invoice, __('Create'));
 
 					if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
-						cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
+						this.frm.add_custom_button(
+							__('Payment'),
+							() => this.make_payment_entry(),
+							__('Create')
+						);
 					}
 
 					if(flt(doc.per_billed) < 100) {
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 3705fcf..390af0d 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -515,6 +515,8 @@
 				parent_dict.update({"customer": parent_dict.get("party_name")})
 
 			self.pricing_rules = []
+			basic_item_details_map = {}
+
 			for item in self.get("items"):
 				if item.get("item_code"):
 					args = parent_dict.copy()
@@ -533,7 +535,17 @@
 					if self.get("is_subcontracted"):
 						args["is_subcontracted"] = self.is_subcontracted
 
-					ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
+					basic_details = basic_item_details_map.get(item.item_code)
+					ret, basic_item_details = get_item_details(
+						args,
+						self,
+						for_validate=True,
+						overwrite_warehouse=False,
+						return_basic_details=True,
+						basic_details=basic_details,
+					)
+
+					basic_item_details_map.setdefault(item.item_code, basic_item_details)
 
 					for fieldname, value in ret.items():
 						if item.meta.get_field(fieldname) and value is not None:
@@ -1232,7 +1244,7 @@
 				)
 			)
 
-	def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield):
+	def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
 		from erpnext.controllers.status_updater import get_allowance_for
 
 		item_allowance = {}
@@ -1245,17 +1257,20 @@
 
 		total_overbilled_amt = 0.0
 
+		reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)]
+		reference_details = self.get_billing_reference_details(
+			reference_names, ref_dt + " Item", based_on
+		)
+
 		for item in self.get("items"):
 			if not item.get(item_ref_dn):
 				continue
 
-			ref_amt = flt(
-				frappe.db.get_value(ref_dt + " Item", item.get(item_ref_dn), based_on),
-				self.precision(based_on, item),
-			)
+			ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
+
 			if not ref_amt:
 				frappe.msgprint(
-					_("System will not check overbilling since amount for Item {0} in {1} is zero").format(
+					_("System will not check over billing since amount for Item {0} in {1} is zero").format(
 						item.item_code, ref_dt
 					),
 					title=_("Warning"),
@@ -1302,6 +1317,16 @@
 				alert=True,
 			)
 
+	def get_billing_reference_details(self, reference_names, reference_doctype, based_on):
+		return frappe._dict(
+			frappe.get_all(
+				reference_doctype,
+				filters={"name": ("in", reference_names)},
+				fields=["name", based_on],
+				as_list=1,
+			)
+		)
+
 	def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
 		"""
 		Returns Sum of Amount of
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 619a415..a085af8 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -943,7 +943,8 @@
 	2) If no value, get last valuation rate from SLE
 	3) If no value, get valuation rate from Item
 	"""
-	from frappe.query_builder.functions import Sum
+	from frappe.query_builder.functions import Count, IfNull, Sum
+	from pypika import Case
 
 	item_code, company = data.get("item_code"), data.get("company")
 	valuation_rate = 0.0
@@ -954,7 +955,14 @@
 		frappe.qb.from_(bin_table)
 		.join(wh_table)
 		.on(bin_table.warehouse == wh_table.name)
-		.select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate"))
+		.select(
+			Case()
+			.when(
+				Count(bin_table.name) > 0, IfNull(Sum(bin_table.stock_value) / Sum(bin_table.actual_qty), 0.0)
+			)
+			.else_(None)
+			.as_("valuation_rate")
+		)
 		.where((bin_table.item_code == item_code) & (wh_table.company == company))
 	).run(as_dict=True)[0]
 
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 2624daa..fdaa4a2 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -344,6 +344,7 @@
   {
    "fieldname": "prod_plan_references",
    "fieldtype": "Table",
+   "hidden": 1,
    "label": "Production Plan Item Reference",
    "options": "Production Plan Item Reference"
   },
@@ -397,7 +398,7 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-11-26 14:51:08.774372",
+ "modified": "2023-03-31 10:30:48.118932",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Production Plan",
diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json
index 84dee4a..15ef207 100644
--- a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json
+++ b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json
@@ -28,7 +28,7 @@
    "fieldname": "qty",
    "fieldtype": "Data",
    "in_list_view": 1,
-   "label": "qty"
+   "label": "Qty"
   },
   {
    "fieldname": "item_reference",
@@ -40,7 +40,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-05-07 17:03:49.707487",
+ "modified": "2023-03-31 10:30:14.604051",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Production Plan Item Reference",
@@ -48,5 +48,6 @@
  "permissions": [],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 8e57ebd..8efc47d 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -135,7 +135,7 @@
 				}
 				else {
 					// allow for '0' qty on Credit/Debit notes
-					let qty = item.qty || me.frm.doc.is_debit_note ? 1 : -1;
+					let qty = item.qty || (me.frm.doc.is_debit_note ? 1 : -1);
 					item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
 				}
 
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 8d69ea0..0bd4d91 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1897,20 +1897,60 @@
 	}
 
 	make_payment_entry() {
+		let via_journal_entry = this.frm.doc.__onload && this.frm.doc.__onload.make_payment_via_journal_entry;
+		if(this.has_discount_in_schedule() && !via_journal_entry) {
+			// If early payment discount is applied, ask user for reference date
+			this.prompt_user_for_reference_date();
+		} else {
+			this.make_mapped_payment_entry();
+		}
+	}
+
+	make_mapped_payment_entry(args) {
+		var me = this;
+		args = args || { "dt": this.frm.doc.doctype, "dn": this.frm.doc.name };
 		return frappe.call({
-			method: cur_frm.cscript.get_method_for_payment(),
-			args: {
-				"dt": cur_frm.doc.doctype,
-				"dn": cur_frm.doc.name
-			},
+			method: me.get_method_for_payment(),
+			args: args,
 			callback: function(r) {
 				var doclist = frappe.model.sync(r.message);
 				frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
-				// cur_frm.refresh_fields()
 			}
 		});
 	}
 
+	prompt_user_for_reference_date(){
+		var me = this;
+		frappe.prompt({
+			label: __("Cheque/Reference Date"),
+			fieldname: "reference_date",
+			fieldtype: "Date",
+			reqd: 1,
+		}, (values) => {
+			let args = {
+				"dt": me.frm.doc.doctype,
+				"dn": me.frm.doc.name,
+				"reference_date": values.reference_date
+			}
+			me.make_mapped_payment_entry(args);
+		},
+		__("Reference Date for Early Payment Discount"),
+		__("Continue")
+		);
+	}
+
+	has_discount_in_schedule() {
+		let is_eligible = in_list(
+			["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"],
+			this.frm.doctype
+		);
+		let has_payment_schedule = this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length;
+		if(!is_eligible || !has_payment_schedule) return false;
+
+		let has_discount = this.frm.doc.payment_schedule.some(row => row.discount_date);
+		return has_discount;
+	}
+
 	make_quality_inspection() {
 		let data = [];
 		const fields = [
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 489ec6e..2df39c8 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -35,7 +35,14 @@
 
 
 @frappe.whitelist()
-def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True):
+def get_item_details(
+	args,
+	doc=None,
+	for_validate=False,
+	overwrite_warehouse=True,
+	return_basic_details=False,
+	basic_details=None,
+):
 	"""
 	args = {
 	        "item_code": "",
@@ -73,7 +80,13 @@
 		if doc.get("doctype") == "Purchase Invoice":
 			args["bill_date"] = doc.get("bill_date")
 
-	out = get_basic_details(args, item, overwrite_warehouse)
+	if not basic_details:
+		out = get_basic_details(args, item, overwrite_warehouse)
+	else:
+		out = basic_details
+
+	basic_details = out.copy()
+
 	get_item_tax_template(args, item, out)
 	out["item_tax_rate"] = get_item_tax_map(
 		args.company,
@@ -141,7 +154,11 @@
 		out.amount = flt(args.qty) * flt(out.rate)
 
 	out = remove_standard_fields(out)
-	return out
+
+	if return_basic_details:
+		return out, basic_details
+	else:
+		return out
 
 
 def remove_standard_fields(details):
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index d84e4c4..7eba35d 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -58,11 +58,11 @@
 
 	def compare_values(self, ref_doc, fields, doc=None):
 		for reference_doctype, ref_dn_list in ref_doc.items():
+			prev_doc_detail_map = self.get_prev_doc_reference_details(
+				ref_dn_list, reference_doctype, fields
+			)
 			for reference_name in ref_dn_list:
-				prevdoc_values = frappe.db.get_value(
-					reference_doctype, reference_name, [d[0] for d in fields], as_dict=1
-				)
-
+				prevdoc_values = prev_doc_detail_map.get(reference_name)
 				if not prevdoc_values:
 					frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name))
 
@@ -70,6 +70,19 @@
 					if prevdoc_values[field] is not None and field not in self.exclude_fields:
 						self.validate_value(field, condition, prevdoc_values[field], doc)
 
+	def get_prev_doc_reference_details(self, reference_names, reference_doctype, fields):
+		prev_doc_detail_map = {}
+		details = frappe.get_all(
+			reference_doctype,
+			filters={"name": ("in", reference_names)},
+			fields=["name"] + [d[0] for d in fields],
+		)
+
+		for d in details:
+			prev_doc_detail_map.setdefault(d.name, d)
+
+		return prev_doc_detail_map
+
 	def validate_rate_with_reference_doc(self, ref_details):
 		if self.get("is_internal_supplier"):
 			return
@@ -77,23 +90,23 @@
 		buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
 
 		if self.doctype in buying_doctypes:
-			action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action")
-			settings_doc = "Buying Settings"
+			action, role_allowed_to_override = frappe.get_cached_value(
+				"Buying Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
+			)
 		else:
-			action = frappe.db.get_single_value("Selling Settings", "maintain_same_rate_action")
-			settings_doc = "Selling Settings"
+			action, role_allowed_to_override = frappe.get_cached_value(
+				"Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
+			)
 
 		for ref_dt, ref_dn_field, ref_link_field in ref_details:
+			reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
+			reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
 			for d in self.get("items"):
 				if d.get(ref_link_field):
-					ref_rate = frappe.db.get_value(ref_dt + " Item", d.get(ref_link_field), "rate")
+					ref_rate = reference_details.get(d.get(ref_link_field))
 
 					if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
 						if action == "Stop":
-							role_allowed_to_override = frappe.db.get_single_value(
-								settings_doc, "role_to_override_stop_action"
-							)
-
 							if role_allowed_to_override not in frappe.get_roles():
 								frappe.throw(
 									_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
@@ -109,6 +122,16 @@
 								indicator="orange",
 							)
 
+	def get_reference_details(self, reference_names, reference_doctype):
+		return frappe._dict(
+			frappe.get_all(
+				reference_doctype,
+				filters={"name": ("in", reference_names)},
+				fields=["name", "rate"],
+				as_list=1,
+			)
+		)
+
 	def get_link_filters(self, for_doctype):
 		if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype):
 			fieldname = self.prev_link_mapper[for_doctype]["fieldname"]