Merge branch 'develop' into demo_data_on_install
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index fb49ef3..d0940c7 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -341,7 +341,7 @@
 		"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
 	)
 
-	accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
+	accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
 
 	def _book_deferred_revenue_or_expense(
 		item,
diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js
index f033b54..3c0eb85 100644
--- a/erpnext/accounts/doctype/account/account.js
+++ b/erpnext/accounts/doctype/account/account.js
@@ -1,67 +1,83 @@
 // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
 // License: GNU General Public License v3. See license.txt
 
-frappe.ui.form.on('Account', {
-	setup: function(frm) {
-		frm.add_fetch('parent_account', 'report_type', 'report_type');
-		frm.add_fetch('parent_account', 'root_type', 'root_type');
+frappe.ui.form.on("Account", {
+	setup: function (frm) {
+		frm.add_fetch("parent_account", "report_type", "report_type");
+		frm.add_fetch("parent_account", "root_type", "root_type");
 	},
-	onload: function(frm) {
-		frm.set_query('parent_account', function(doc) {
+	onload: function (frm) {
+		frm.set_query("parent_account", function (doc) {
 			return {
 				filters: {
-					"is_group": 1,
-					"company": doc.company
-				}
+					is_group: 1,
+					company: doc.company,
+				},
 			};
 		});
 	},
-	refresh: function(frm) {
-		frm.toggle_display('account_name', frm.is_new());
+	refresh: function (frm) {
+		frm.toggle_display("account_name", frm.is_new());
 
 		// hide fields if group
-		frm.toggle_display(['account_type', 'tax_rate'], cint(frm.doc.is_group) == 0);
+		frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0);
 
 		// disable fields
-		frm.toggle_enable(['is_group', 'company'], false);
+		frm.toggle_enable(["is_group", "company"], false);
 
 		if (cint(frm.doc.is_group) == 0) {
-			frm.toggle_display('freeze_account', frm.doc.__onload
-				&& frm.doc.__onload.can_freeze_account);
+			frm.toggle_display(
+				"freeze_account",
+				frm.doc.__onload && frm.doc.__onload.can_freeze_account
+			);
 		}
 
 		// read-only for root accounts
 		if (!frm.is_new()) {
 			if (!frm.doc.parent_account) {
 				frm.set_read_only();
-				frm.set_intro(__("This is a root account and cannot be edited."));
+				frm.set_intro(
+					__("This is a root account and cannot be edited.")
+				);
 			} else {
 				// credit days and type if customer or supplier
 				frm.set_intro(null);
-				frm.trigger('account_type');
+				frm.trigger("account_type");
 				// show / hide convert buttons
-				frm.trigger('add_toolbar_buttons');
+				frm.trigger("add_toolbar_buttons");
 			}
-			if (frm.has_perm('write')) {
-				frm.add_custom_button(__('Merge Account'), function () {
-					frm.trigger("merge_account");
-				}, __('Actions'));
-				frm.add_custom_button(__('Update Account Name / Number'), function () {
-					frm.trigger("update_account_number");
-				}, __('Actions'));
+			if (frm.has_perm("write")) {
+				frm.add_custom_button(
+					__("Merge Account"),
+					function () {
+						frm.trigger("merge_account");
+					},
+					__("Actions")
+				);
+				frm.add_custom_button(
+					__("Update Account Name / Number"),
+					function () {
+						frm.trigger("update_account_number");
+					},
+					__("Actions")
+				);
 			}
 		}
 	},
 	account_type: function (frm) {
 		if (frm.doc.is_group == 0) {
-			frm.toggle_display(['tax_rate'], frm.doc.account_type == 'Tax');
-			frm.toggle_display('warehouse', frm.doc.account_type == 'Stock');
+			frm.toggle_display(["tax_rate"], frm.doc.account_type == "Tax");
+			frm.toggle_display("warehouse", frm.doc.account_type == "Stock");
 		}
 	},
-	add_toolbar_buttons: function(frm) {
-		frm.add_custom_button(__('Chart of Accounts'), () => {
-			frappe.set_route("Tree", "Account");
-		}, __('View'));
+	add_toolbar_buttons: function (frm) {
+		frm.add_custom_button(
+			__("Chart of Accounts"),
+			() => {
+				frappe.set_route("Tree", "Account");
+			},
+			__("View")
+		);
 
 		if (frm.doc.is_group == 1) {
 			frm.add_custom_button(__('Convert to Non-Group'), function () {
@@ -86,31 +102,35 @@
 				frappe.set_route("query-report", "General Ledger");
 			}, __('View'));
 
-			frm.add_custom_button(__('Convert to Group'), function () {
-				return frappe.call({
-					doc: frm.doc,
-					method: 'convert_ledger_to_group',
-					callback: function() {
-						frm.refresh();
-					}
-				});
-			}, __('Actions'));
+			frm.add_custom_button(
+				__("Convert to Group"),
+				function () {
+					return frappe.call({
+						doc: frm.doc,
+						method: "convert_ledger_to_group",
+						callback: function () {
+							frm.refresh();
+						},
+					});
+				},
+				__("Actions")
+			);
 		}
 	},
 
-	merge_account: function(frm) {
+	merge_account: function (frm) {
 		var d = new frappe.ui.Dialog({
-			title: __('Merge with Existing Account'),
+			title: __("Merge with Existing Account"),
 			fields: [
 				{
-					"label" : "Name",
-					"fieldname": "name",
-					"fieldtype": "Data",
-					"reqd": 1,
-					"default": frm.doc.name
-				}
+					label: "Name",
+					fieldname: "name",
+					fieldtype: "Data",
+					reqd: 1,
+					default: frm.doc.name,
+				},
 			],
-			primary_action: function() {
+			primary_action: function () {
 				var data = d.get_values();
 				frappe.call({
 					method: "erpnext.accounts.doctype.account.account.merge_account",
@@ -119,44 +139,47 @@
 						new: data.name,
 						is_group: frm.doc.is_group,
 						root_type: frm.doc.root_type,
-						company: frm.doc.company
+						company: frm.doc.company,
 					},
-					callback: function(r) {
-						if(!r.exc) {
-							if(r.message) {
+					callback: function (r) {
+						if (!r.exc) {
+							if (r.message) {
 								frappe.set_route("Form", "Account", r.message);
 							}
 							d.hide();
 						}
-					}
+					},
 				});
 			},
-			primary_action_label: __('Merge')
+			primary_action_label: __("Merge"),
 		});
 		d.show();
 	},
 
-	update_account_number: function(frm) {
+	update_account_number: function (frm) {
 		var d = new frappe.ui.Dialog({
-			title: __('Update Account Number / Name'),
+			title: __("Update Account Number / Name"),
 			fields: [
 				{
-					"label": "Account Name",
-					"fieldname": "account_name",
-					"fieldtype": "Data",
-					"reqd": 1,
-					"default": frm.doc.account_name
+					label: "Account Name",
+					fieldname: "account_name",
+					fieldtype: "Data",
+					reqd: 1,
+					default: frm.doc.account_name,
 				},
 				{
-					"label": "Account Number",
-					"fieldname": "account_number",
-					"fieldtype": "Data",
-					"default": frm.doc.account_number
-				}
+					label: "Account Number",
+					fieldname: "account_number",
+					fieldtype: "Data",
+					default: frm.doc.account_number,
+				},
 			],
-			primary_action: function() {
+			primary_action: function () {
 				var data = d.get_values();
-				if(data.account_number === frm.doc.account_number && data.account_name === frm.doc.account_name) {
+				if (
+					data.account_number === frm.doc.account_number &&
+					data.account_name === frm.doc.account_name
+				) {
 					d.hide();
 					return;
 				}
@@ -166,23 +189,29 @@
 					args: {
 						account_number: data.account_number,
 						account_name: data.account_name,
-						name: frm.doc.name
+						name: frm.doc.name,
 					},
-					callback: function(r) {
-						if(!r.exc) {
-							if(r.message) {
+					callback: function (r) {
+						if (!r.exc) {
+							if (r.message) {
 								frappe.set_route("Form", "Account", r.message);
 							} else {
-								frm.set_value("account_number", data.account_number);
-								frm.set_value("account_name", data.account_name);
+								frm.set_value(
+									"account_number",
+									data.account_number
+								);
+								frm.set_value(
+									"account_name",
+									data.account_name
+								);
 							}
 							d.hide();
 						}
-					}
+					},
 				});
 			},
-			primary_action_label: __('Update')
+			primary_action_label: __("Update"),
 		});
 		d.show();
-	}
+	},
 });
diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json
index e79fb66..78f73ef 100644
--- a/erpnext/accounts/doctype/account/account.json
+++ b/erpnext/accounts/doctype/account/account.json
@@ -123,7 +123,7 @@
    "label": "Account Type",
    "oldfieldname": "account_type",
    "oldfieldtype": "Select",
-   "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
+   "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
   },
   {
    "description": "Rate at which this tax is applied",
@@ -192,7 +192,7 @@
  "idx": 1,
  "is_tree": 1,
  "links": [],
- "modified": "2023-04-11 16:08:46.983677",
+ "modified": "2023-07-20 18:18:44.405723",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Account",
@@ -243,7 +243,6 @@
    "read": 1,
    "report": 1,
    "role": "Accounts Manager",
-   "set_user_permissions": 1,
    "share": 1,
    "write": 1
   }
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index e94b7cf..c1eca72 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -45,6 +45,7 @@
 		if frappe.local.flags.allow_unverified_charts:
 			return
 		self.validate_parent()
+		self.validate_parent_child_account_type()
 		self.validate_root_details()
 		validate_field_number("Account", self.name, self.account_number, self.company, "account_number")
 		self.validate_group_or_ledger()
@@ -55,6 +56,20 @@
 		self.validate_account_currency()
 		self.validate_root_company_and_sync_account_to_children()
 
+	def validate_parent_child_account_type(self):
+		if self.parent_account:
+			if self.account_type in [
+				"Direct Income",
+				"Indirect Income",
+				"Current Asset",
+				"Current Liability",
+				"Direct Expense",
+				"Indirect Expense",
+			]:
+				parent_account_type = frappe.db.get_value("Account", self.parent_account, ["account_type"])
+				if parent_account_type == self.account_type:
+					throw(_("Only Parent can be of type {0}").format(self.account_type))
+
 	def validate_parent(self):
 		"""Fetch Parent Details and validate parent account"""
 		if self.parent_account:
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.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 8d8cbef..35a3788 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -8,7 +8,7 @@
 frappe.ui.form.on("Journal Entry", {
 	setup: function(frm) {
 		frm.add_fetch("bank_account", "account", "account");
-		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule'];
+		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger"];
 	},
 
 	refresh: function(frm) {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index ea4a2d4..22e092c 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,15 +88,16 @@
 		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",
 			"Payment Ledger Entry",
 			"Repost Payment Ledger",
 			"Repost Payment Ledger Items",
+			"Repost Accounting Ledger",
+			"Repost Accounting Ledger Items",
 		)
 		self.make_gl_entries(1)
 		self.update_advance_paid()
@@ -499,11 +501,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 +589,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 +773,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 +945,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/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
index 48a25ad..a134f74 100644
--- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
+++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
@@ -141,7 +141,7 @@
 		)
 
 		if points_to_redeem > loyalty_program_details.loyalty_points:
-			frappe.throw(_("You don't have enought Loyalty Points to redeem"))
+			frappe.throw(_("You don't have enough Loyalty Points to redeem"))
 
 		loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor)
 
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 44474d9..f131be2 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','Repost Accounting 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 21adb27..64b4d16 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,
@@ -142,7 +147,10 @@
 			"Payment Ledger Entry",
 			"Repost Payment Ledger",
 			"Repost Payment Ledger Items",
+			"Repost Accounting Ledger",
+			"Repost Accounting Ledger Items",
 		)
+		super(PaymentEntry, self).on_cancel()
 		self.make_gl_entries(cancel=1)
 		self.make_advance_gl_entries(cancel=1)
 		self.update_outstanding_amounts()
@@ -277,12 +285,14 @@
 
 			fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
 
-			if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
-				frappe.throw(fail_message.format(d.idx))
-
-			if d.payment_term and (
-				(flt(d.allocated_amount)) > 0
-				and flt(d.allocated_amount) > flt(latest.payment_term_outstanding)
+			if (
+				d.payment_term
+				and (
+					(flt(d.allocated_amount)) > 0
+					and latest.payment_term_outstanding
+					and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
+				)
+				and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
 			):
 				frappe.throw(
 					_(
@@ -292,6 +302,9 @@
 					)
 				)
 
+			if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
+				frappe.throw(fail_message.format(d.idx))
+
 			# Check for negative outstanding invoices as well
 			if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
 				frappe.throw(fail_message.format(d.idx))
@@ -399,7 +412,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(
@@ -412,7 +425,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(
@@ -677,7 +690,9 @@
 		if not self.apply_tax_withholding_amount:
 			return
 
-		net_total = self.paid_amount
+		order_amount = self.get_order_net_total()
+
+		net_total = flt(order_amount) + flt(self.unallocated_amount)
 
 		# Adding args as purchase invoice to get TDS amount
 		args = frappe._dict(
@@ -722,6 +737,20 @@
 		for d in to_remove:
 			self.remove(d)
 
+	def get_order_net_total(self):
+		if self.party_type == "Supplier":
+			doctype = "Purchase Order"
+		else:
+			doctype = "Sales Order"
+
+		docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
+
+		tax_withholding_net_total = frappe.db.get_value(
+			doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"]
+		)
+
+		return tax_withholding_net_total
+
 	def apply_taxes(self):
 		self.initialize_taxes()
 		self.determine_exclusive_rate()
@@ -808,10 +837,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):
@@ -1002,6 +1046,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:
@@ -1988,7 +2036,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) >= (
@@ -2128,7 +2175,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 c6e93f3..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)
 
@@ -1156,6 +1155,52 @@
 		si3.cancel()
 		si3.delete()
 
+	@change_settings(
+		"Accounts Settings",
+		{
+			"unlink_payment_on_cancellation_of_invoice": 1,
+			"delete_linked_ledger_entries": 1,
+			"allow_multi_currency_invoices_against_single_party_account": 1,
+		},
+	)
+	def test_overallocation_validation_shouldnt_misfire(self):
+		"""
+		Overallocation validation shouldn't fire for Template without "Allocate Payment based on Payment Terms" enabled
+
+		"""
+		customer = create_customer()
+		create_payment_terms_template()
+
+		template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
+		template.allocate_payment_based_on_payment_terms = 0
+		template.save()
+
+		# Validate allocation on base/company currency
+		si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
+		si.payment_terms_template = "Test Receivable Template"
+		si.save().submit()
+
+		si.reload()
+		pe = get_payment_entry(si.doctype, si.name).save()
+		# There will no term based allocation
+		self.assertEqual(len(pe.references), 1)
+		self.assertEqual(pe.references[0].payment_term, None)
+		self.assertEqual(flt(pe.references[0].allocated_amount), flt(si.grand_total))
+		pe.save()
+
+		# specify a term
+		pe.references[0].payment_term = template.terms[0].payment_term
+		# no validation error should be thrown
+		pe.save()
+
+		pe.paid_amount = si.grand_total + 1
+		pe.references[0].allocated_amount = si.grand_total + 1
+		self.assertRaises(frappe.ValidationError, pe.save)
+
+		template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
+		template.allocate_payment_based_on_payment_terms = 1
+		template.save()
+
 
 def create_payment_entry(**args):
 	payment_entry = frappe.new_doc("Payment Entry")
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.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 89d6207..66438a7 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -35,7 +35,7 @@
 		super.onload();
 
 		// Ignore linked advances
-		this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"];
+		this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"];
 
 		if(!this.frm.doc.__islocal) {
 			// show credit_to in print format
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index d8759e9..0599e19 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -167,6 +167,7 @@
   "column_break_63",
   "unrealized_profit_loss_account",
   "subscription_section",
+  "subscription",
   "auto_repeat",
   "update_auto_repeat_reference",
   "column_break_114",
@@ -1424,6 +1425,12 @@
    "read_only": 1
   },
   {
+   "fieldname": "subscription",
+   "fieldtype": "Link",
+   "label": "Subscription",
+   "options": "Subscription"
+  },
+  {
    "default": "0",
    "fieldname": "is_old_subcontracting_flow",
    "fieldtype": "Check",
@@ -1577,7 +1584,7 @@
  "idx": 204,
  "is_submittable": 1,
  "links": [],
- "modified": "2023-07-04 17:22:59.145031",
+ "modified": "2023-07-25 17:22:59.145031",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 230a8b3..f334399 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -229,7 +229,7 @@
 		)
 
 		if (
-			cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))
+			cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate"))
 			and not self.is_return
 			and not self.is_internal_supplier
 		):
@@ -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)
@@ -969,30 +969,6 @@
 							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, self.use_company_roundoff_cost_center
-		)
-
-		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": round_off_cost_center
-						if self.use_company_roundoff_cost_center
-						else 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")
@@ -1439,6 +1415,8 @@
 			"Repost Item Valuation",
 			"Repost Payment Ledger",
 			"Repost Payment Ledger Items",
+			"Repost Accounting Ledger",
+			"Repost Accounting Ledger Items",
 			"Payment Ledger Entry",
 			"Tax Withheld Vouchers",
 			"Serial and Batch Bundle",
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 486e01e..ce7ada3 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()
 
@@ -1736,6 +1791,52 @@
 		rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
 		self.assertAlmostEqual(returned_inv.items[0].rate, rate)
 
+	def test_payment_allocation_for_payment_terms(self):
+		from erpnext.buying.doctype.purchase_order.test_purchase_order import (
+			create_pr_against_po,
+			create_purchase_order,
+		)
+		from erpnext.selling.doctype.sales_order.test_sales_order import (
+			automatically_fetch_payment_terms,
+		)
+		from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
+			make_purchase_invoice as make_pi_from_pr,
+		)
+
+		automatically_fetch_payment_terms()
+		frappe.db.set_value(
+			"Payment Terms Template",
+			"_Test Payment Term Template",
+			"allocate_payment_based_on_payment_terms",
+			0,
+		)
+
+		po = create_purchase_order(do_not_save=1)
+		po.payment_terms_template = "_Test Payment Term Template"
+		po.save()
+		po.submit()
+
+		pr = create_pr_against_po(po.name, received_qty=4)
+		pi = make_pi_from_pr(pr.name)
+		self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
+
+		frappe.db.set_value(
+			"Payment Terms Template",
+			"_Test Payment Term Template",
+			"allocate_payment_based_on_payment_terms",
+			1,
+		)
+		pi = make_pi_from_pr(pr.name)
+		self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
+
+		automatically_fetch_payment_terms(enable=0)
+		frappe.db.set_value(
+			"Payment Terms Template",
+			"_Test Payment Term Template",
+			"allocate_payment_based_on_payment_terms",
+			0,
+		)
+
 	def test_offsetting_entries_for_accounting_dimensions(self):
 		from erpnext.accounts.doctype.account.test_account import create_account
 		from erpnext.accounts.report.trial_balance.test_trial_balance import (
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html
new file mode 100644
index 0000000..2dec8f7
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html
@@ -0,0 +1,44 @@
+<style>
+	.print-format {
+		padding: 4mm;
+		font-size: 8.0pt !important;
+	}
+	.print-format td {
+		vertical-align:middle !important;
+	}
+	.old {
+	    background-color: #FFB3C0;
+	}
+	.new {
+	    background-color: #B3FFCC;
+	}
+</style>
+
+
+<table class="table table-bordered table-condensed">
+  <colgroup>
+  {% for col in gl_columns%}
+  <col style="width: 18mm;">
+  {% endfor %}
+  </colgroup>
+  <thead>
+    <tr>
+    {% for col in gl_columns%}
+    <td>{{ col.label }}</td>
+    {% endfor %}
+    </tr>
+  </thead>
+{% for gl in gl_data%}
+{% if gl["old"]%}
+<tr class="old">
+{% else %}
+<tr class="new">
+{% endif %}
+  {% for col in gl_columns %}
+  <td class="text-right">
+    {{ gl[col.fieldname] }}
+  </td>
+  {% endfor %}
+</tr>
+{% endfor %}
+</table>
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js
new file mode 100644
index 0000000..3a87a38
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js
@@ -0,0 +1,50 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Repost Accounting Ledger", {
+	setup: function(frm) {
+		frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
+			return {
+				filters: {
+					name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']],
+				}
+			}
+		}
+
+		frm.fields_dict['vouchers'].grid.get_field('voucher_no').get_query = function(doc) {
+			if (doc.company) {
+				return {
+					filters: {
+						company: doc.company,
+						docstatus: 1
+					}
+				}
+			}
+		}
+	},
+
+	refresh: function(frm) {
+		frm.add_custom_button(__('Show Preview'), () => {
+			frm.call({
+				method: 'generate_preview',
+				doc: frm.doc,
+				freeze: true,
+				freeze_message: __('Generating Preview'),
+				callback: function(r) {
+					if (r && r.message) {
+						let content = r.message;
+						let opts = {
+							title: "Preview",
+							subtitle: "preview",
+							content: content,
+							print_settings: {orientation: "landscape"},
+							columns: [],
+							data: [],
+						}
+						frappe.render_grid(opts);
+					}
+				}
+			});
+		});
+	}
+});
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
new file mode 100644
index 0000000..8d56c9b
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
@@ -0,0 +1,81 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format:ACC-REPOST-{#####}",
+ "creation": "2023-07-04 13:07:32.923675",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "company",
+  "column_break_vpup",
+  "delete_cancelled_entries",
+  "section_break_metl",
+  "vouchers",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "label": "Company",
+   "options": "Company"
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Repost Accounting Ledger",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "vouchers",
+   "fieldtype": "Table",
+   "label": "Vouchers",
+   "options": "Repost Accounting Ledger Items"
+  },
+  {
+   "fieldname": "column_break_vpup",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_metl",
+   "fieldtype": "Section Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "delete_cancelled_entries",
+   "fieldtype": "Check",
+   "label": "Delete Cancelled Ledger Entries"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-07-27 15:47:58.975034",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Repost Accounting Ledger",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
new file mode 100644
index 0000000..4cf2ed2
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
@@ -0,0 +1,183 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _, qb
+from frappe.model.document import Document
+from frappe.utils.data import comma_and
+
+
+class RepostAccountingLedger(Document):
+	def __init__(self, *args, **kwargs):
+		super(RepostAccountingLedger, self).__init__(*args, **kwargs)
+		self._allowed_types = set(
+			["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]
+		)
+
+	def validate(self):
+		self.validate_vouchers()
+		self.validate_for_closed_fiscal_year()
+		self.validate_for_deferred_accounting()
+
+	def validate_for_deferred_accounting(self):
+		sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"]
+		docs_with_deferred_revenue = frappe.db.get_all(
+			"Sales Invoice Item",
+			filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
+			fields=["parent"],
+			as_list=1,
+		)
+
+		purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"]
+		docs_with_deferred_expense = frappe.db.get_all(
+			"Purchase Invoice Item",
+			filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
+			fields=["parent"],
+			as_list=1,
+		)
+
+		if docs_with_deferred_revenue or docs_with_deferred_expense:
+			frappe.throw(
+				_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
+					frappe.bold(
+						comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])
+					)
+				)
+			)
+
+	def validate_for_closed_fiscal_year(self):
+		if self.vouchers:
+			latest_pcv = (
+				frappe.db.get_all(
+					"Period Closing Voucher",
+					filters={"company": self.company},
+					order_by="posting_date desc",
+					pluck="posting_date",
+					limit=1,
+				)
+				or None
+			)
+			if not latest_pcv:
+				return
+
+			for vtype in self._allowed_types:
+				if names := [x.voucher_no for x in self.vouchers if x.voucher_type == vtype]:
+					latest_voucher = frappe.db.get_all(
+						vtype,
+						filters={"name": ["in", names]},
+						pluck="posting_date",
+						order_by="posting_date desc",
+						limit=1,
+					)[0]
+					if latest_voucher and latest_pcv[0] >= latest_voucher:
+						frappe.throw(_("Cannot Resubmit Ledger entries for vouchers in Closed fiscal year."))
+
+	def validate_vouchers(self):
+		if self.vouchers:
+			# Validate voucher types
+			voucher_types = set([x.voucher_type for x in self.vouchers])
+			if disallowed_types := voucher_types.difference(self._allowed_types):
+				frappe.throw(
+					_("{0} types are not allowed. Only {1} are.").format(
+						frappe.bold(comma_and(list(disallowed_types))),
+						frappe.bold(comma_and(list(self._allowed_types))),
+					)
+				)
+
+	def get_existing_ledger_entries(self):
+		vouchers = [x.voucher_no for x in self.vouchers]
+		gl = qb.DocType("GL Entry")
+		existing_gles = (
+			qb.from_(gl)
+			.select(gl.star)
+			.where((gl.voucher_no.isin(vouchers)) & (gl.is_cancelled == 0))
+			.run(as_dict=True)
+		)
+		self.gles = frappe._dict({})
+
+		for gle in existing_gles:
+			self.gles.setdefault((gle.voucher_type, gle.voucher_no), frappe._dict({})).setdefault(
+				"existing", []
+			).append(gle.update({"old": True}))
+
+	def generate_preview_data(self):
+		self.gl_entries = []
+		self.get_existing_ledger_entries()
+		for x in self.vouchers:
+			doc = frappe.get_doc(x.voucher_type, x.voucher_no)
+			if doc.doctype in ["Payment Entry", "Journal Entry"]:
+				gle_map = doc.build_gl_map()
+			else:
+				gle_map = doc.get_gl_entries()
+
+			old_entries = self.gles.get((x.voucher_type, x.voucher_no))
+			if old_entries:
+				self.gl_entries.extend(old_entries.existing)
+			self.gl_entries.extend(gle_map)
+
+	@frappe.whitelist()
+	def generate_preview(self):
+		from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
+
+		gl_columns = []
+		gl_data = []
+
+		self.generate_preview_data()
+		if self.gl_entries:
+			filters = {"company": self.company, "include_dimensions": 1}
+			for x in get_gl_columns(filters):
+				if x["fieldname"] == "gl_entry":
+					x["fieldname"] = "name"
+				gl_columns.append(x)
+
+			gl_data = self.gl_entries
+		rendered_page = frappe.render_template(
+			"erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html",
+			{"gl_columns": gl_columns, "gl_data": gl_data},
+		)
+
+		return rendered_page
+
+	def on_submit(self):
+		job_name = "repost_accounting_ledger_" + self.name
+		frappe.enqueue(
+			method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
+			account_repost_doc=self.name,
+			is_async=True,
+			job_name=job_name,
+		)
+		frappe.msgprint(_("Repost has started in the background"))
+
+
+@frappe.whitelist()
+def start_repost(account_repost_doc=str) -> None:
+	if account_repost_doc:
+		repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
+
+		if repost_doc.docstatus == 1:
+			# Prevent repost on invoices with deferred accounting
+			repost_doc.validate_for_deferred_accounting()
+
+			for x in repost_doc.vouchers:
+				doc = frappe.get_doc(x.voucher_type, x.voucher_no)
+
+				if repost_doc.delete_cancelled_entries:
+					frappe.db.delete("GL Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name})
+					frappe.db.delete(
+						"Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}
+					)
+
+				if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
+					if not repost_doc.delete_cancelled_entries:
+						doc.docstatus = 2
+						doc.make_gl_entries_on_cancel()
+
+					doc.docstatus = 1
+					doc.make_gl_entries()
+
+				elif doc.doctype in ["Payment Entry", "Journal Entry"]:
+					if not repost_doc.delete_cancelled_entries:
+						doc.make_gl_entries(1)
+					doc.make_gl_entries()
+
+				frappe.db.commit()
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
new file mode 100644
index 0000000..0e75dd2
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
@@ -0,0 +1,202 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+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, nowdate, today
+
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
+from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import start_repost
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+from erpnext.accounts.utils import get_fiscal_year
+
+
+class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
+	def setUp(self):
+		self.create_company()
+		self.create_customer()
+		self.create_item()
+
+	def teadDown(self):
+		frappe.db.rollback()
+
+	def test_01_basic_functions(self):
+		si = create_sales_invoice(
+			item=self.item,
+			company=self.company,
+			customer=self.customer,
+			debit_to=self.debit_to,
+			parent_cost_center=self.cost_center,
+			cost_center=self.cost_center,
+			rate=100,
+		)
+
+		preq = frappe.get_doc(
+			make_payment_request(
+				dt=si.doctype,
+				dn=si.name,
+				payment_request_type="Inward",
+				party_type="Customer",
+				party=si.customer,
+			)
+		)
+		preq.save().submit()
+
+		# Test Validation Error
+		ral = frappe.new_doc("Repost Accounting Ledger")
+		ral.company = self.company
+		ral.delete_cancelled_entries = True
+		ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+		ral.append(
+			"vouchers", {"voucher_type": preq.doctype, "voucher_no": preq.name}
+		)  # this should throw validation error
+		self.assertRaises(frappe.ValidationError, ral.save)
+		ral.vouchers.pop()
+		preq.cancel()
+		preq.delete()
+
+		pe = get_payment_entry(si.doctype, si.name)
+		pe.save().submit()
+		ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
+		ral.save()
+
+		# manually set an incorrect debit amount in DB
+		gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to})
+		frappe.db.set_value("GL Entry", gle[0], "debit", 90)
+
+		gl = qb.DocType("GL Entry")
+		res = (
+			qb.from_(gl)
+			.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
+			.where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
+			.run()
+		)
+
+		# Assert incorrect ledger balance
+		self.assertNotEqual(res[0], (si.name, 100, 100))
+
+		# Submit repost document
+		ral.save().submit()
+
+		# background jobs don't run on test cases. Manually triggering repost function.
+		start_repost(ral.name)
+
+		res = (
+			qb.from_(gl)
+			.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
+			.where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
+			.run()
+		)
+
+		# Ledger should reflect correct amount post repost
+		self.assertEqual(res[0], (si.name, 100, 100))
+
+	def test_02_deferred_accounting_valiations(self):
+		si = create_sales_invoice(
+			item=self.item,
+			company=self.company,
+			customer=self.customer,
+			debit_to=self.debit_to,
+			parent_cost_center=self.cost_center,
+			cost_center=self.cost_center,
+			rate=100,
+			do_not_submit=True,
+		)
+		si.items[0].enable_deferred_revenue = True
+		si.items[0].deferred_revenue_account = self.deferred_revenue
+		si.items[0].service_start_date = nowdate()
+		si.items[0].service_end_date = add_days(nowdate(), 90)
+		si.save().submit()
+
+		ral = frappe.new_doc("Repost Accounting Ledger")
+		ral.company = self.company
+		ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+		self.assertRaises(frappe.ValidationError, ral.save)
+
+	@change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
+	def test_04_pcv_validation(self):
+		# Clear old GL entries so PCV can be submitted.
+		gl = frappe.qb.DocType("GL Entry")
+		qb.from_(gl).delete().where(gl.company == self.company).run()
+
+		si = create_sales_invoice(
+			item=self.item,
+			company=self.company,
+			customer=self.customer,
+			debit_to=self.debit_to,
+			parent_cost_center=self.cost_center,
+			cost_center=self.cost_center,
+			rate=100,
+		)
+		pcv = frappe.get_doc(
+			{
+				"doctype": "Period Closing Voucher",
+				"transaction_date": today(),
+				"posting_date": today(),
+				"company": self.company,
+				"fiscal_year": get_fiscal_year(today(), company=self.company)[0],
+				"cost_center": self.cost_center,
+				"closing_account_head": self.retained_earnings,
+				"remarks": "test",
+			}
+		)
+		pcv.save().submit()
+
+		ral = frappe.new_doc("Repost Accounting Ledger")
+		ral.company = self.company
+		ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+		self.assertRaises(frappe.ValidationError, ral.save)
+
+		pcv.reload()
+		pcv.cancel()
+		pcv.delete()
+
+	def test_03_deletion_flag_and_preview_function(self):
+		si = create_sales_invoice(
+			item=self.item,
+			company=self.company,
+			customer=self.customer,
+			debit_to=self.debit_to,
+			parent_cost_center=self.cost_center,
+			cost_center=self.cost_center,
+			rate=100,
+		)
+
+		pe = get_payment_entry(si.doctype, si.name)
+		pe.save().submit()
+
+		# without deletion flag set
+		ral = frappe.new_doc("Repost Accounting Ledger")
+		ral.company = self.company
+		ral.delete_cancelled_entries = False
+		ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+		ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
+		ral.save()
+
+		# assert preview data is generated
+		preview = ral.generate_preview()
+		self.assertIsNotNone(preview)
+
+		ral.save().submit()
+
+		# background jobs don't run on test cases. Manually triggering repost function.
+		start_repost(ral.name)
+
+		self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
+		self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
+
+		# with deletion flag set
+		ral = frappe.new_doc("Repost Accounting Ledger")
+		ral.company = self.company
+		ral.delete_cancelled_entries = True
+		ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+		ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
+		ral.save().submit()
+
+		start_repost(ral.name)
+		self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
+		self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json
new file mode 100644
index 0000000..4a2041f
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json
@@ -0,0 +1,40 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-07-04 14:14:01.243848",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "voucher_type",
+  "voucher_no"
+ ],
+ "fields": [
+  {
+   "fieldname": "voucher_type",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Voucher Type",
+   "options": "DocType"
+  },
+  {
+   "fieldname": "voucher_no",
+   "fieldtype": "Dynamic Link",
+   "in_list_view": 1,
+   "label": "Voucher No",
+   "options": "voucher_type"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-07-04 14:15:51.165584",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Repost Accounting Ledger Items",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py
new file mode 100644
index 0000000..9221f44
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class RepostAccountingLedgerItems(Document):
+	pass
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index b45bc41..a4bcdb4 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -37,7 +37,7 @@
 		super.onload();
 
 		this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
-							  'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"];
+							  'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
 
 		if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
 			// show debit_to in print format
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index f0d3f72..7581366 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -194,6 +194,7 @@
   "select_print_heading",
   "language",
   "subscription_section",
+  "subscription",
   "from_date",
   "auto_repeat",
   "column_break_140",
@@ -2018,6 +2019,12 @@
    "read_only": 1
   },
   {
+   "fieldname": "subscription",
+   "fieldtype": "Link",
+   "label": "Subscription",
+   "options": "Subscription"
+  },
+  {
    "default": "0",
    "depends_on": "eval: doc.apply_discount_on == \"Grand Total\"",
    "fieldname": "is_cash_or_non_trade_discount",
@@ -2157,7 +2164,7 @@
    "link_fieldname": "consolidated_invoice"
   }
  ],
- "modified": "2023-06-21 16:02:18.988799",
+ "modified": "2023-07-25 16:02:18.988799",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index f5ee228..0bc5aa2 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,
@@ -32,6 +32,7 @@
 	reset_depreciation_schedule,
 	reverse_depreciation_entry_made_after_disposal,
 )
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 from erpnext.controllers.accounts_controller import validate_account_head
 from erpnext.controllers.selling_controller import SellingController
 from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
@@ -385,6 +386,8 @@
 			"Repost Item Valuation",
 			"Repost Payment Ledger",
 			"Repost Payment Ledger Items",
+			"Repost Accounting Ledger",
+			"Repost Accounting Ledger Items",
 			"Payment Ledger Entry",
 			"Serial and Batch Bundle",
 		)
@@ -1029,7 +1032,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":
@@ -1054,10 +1060,10 @@
 		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)
+		self.make_precision_loss_gl_entry(gl_entries)
 		self.make_discount_gl_entries(gl_entries)
 
 		# merge gl entries before adding pos entries
@@ -1176,12 +1182,13 @@
 							self.get("posting_date"),
 						)
 						asset.db_set("disposal_date", None)
+						add_asset_activity(asset.name, _("Asset returned"))
 
 						if asset.calculate_depreciation:
 							posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
 							reverse_depreciation_entry_made_after_disposal(asset, posting_date)
 							notes = _(
-								"This schedule was created when Asset {0} was returned after being sold through Sales Invoice {1}."
+								"This schedule was created when Asset {0} was returned through Sales Invoice {1}."
 							).format(
 								get_link_to_form(asset.doctype, asset.name),
 								get_link_to_form(self.doctype, self.get("name")),
@@ -1209,6 +1216,7 @@
 							self.get("posting_date"),
 						)
 						asset.db_set("disposal_date", self.posting_date)
+						add_asset_activity(asset.name, _("Asset sold"))
 
 					for gle in fixed_asset_gl_entries:
 						gle["against"] = self.customer
@@ -1646,15 +1654,13 @@
 		frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name)
 
 	def get_returned_amount(self):
-		from frappe.query_builder.functions import Coalesce, Sum
+		from frappe.query_builder.functions import Sum
 
 		doc = frappe.qb.DocType(self.doctype)
 		returned_amount = (
 			frappe.qb.from_(doc)
 			.select(Sum(doc.grand_total))
-			.where(
-				(doc.docstatus == 1) & (doc.is_return == 1) & (Coalesce(doc.return_against, "") == self.name)
-			)
+			.where((doc.docstatus == 1) & (doc.is_return == 1) & (doc.return_against == self.name))
 		).run()
 
 		return abs(returned_amount[0][0]) if returned_amount[0][0] else 0
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
index 0a765f3..6fdcf26 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
@@ -15,6 +15,7 @@
 		},
 		"internal_links": {
 			"Sales Order": ["items", "sales_order"],
+			"Delivery Note": ["items", "delivery_note"],
 			"Timesheet": ["timesheets", "time_sheet"],
 		},
 		"transactions": [
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 41e5554..63c0c45 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2049,28 +2049,27 @@
 		self.assertEqual(si.total_taxes_and_charges, 228.82)
 		self.assertEqual(si.rounding_adjustment, -0.01)
 
-		expected_values = dict(
-			(d[0], d)
-			for d in [
-				[si.debit_to, 1500, 0.0],
-				["_Test Account Service Tax - _TC", 0.0, 114.41],
-				["_Test Account VAT - _TC", 0.0, 114.41],
-				["Sales - _TC", 0.0, 1271.18],
-			]
-		)
+		expected_values = [
+			["_Test Account Service Tax - _TC", 0.0, 114.41],
+			["_Test Account VAT - _TC", 0.0, 114.41],
+			[si.debit_to, 1500, 0.0],
+			["Round Off - _TC", 0.01, 0.01],
+			["Sales - _TC", 0.0, 1271.18],
+		]
 
 		gl_entries = frappe.db.sql(
-			"""select account, debit, credit
+			"""select account, sum(debit) as debit, sum(credit) as credit
 			from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
+			group by account
 			order by account asc""",
 			si.name,
 			as_dict=1,
 		)
 
-		for gle in gl_entries:
-			self.assertEqual(expected_values[gle.account][0], gle.account)
-			self.assertEqual(expected_values[gle.account][1], gle.debit)
-			self.assertEqual(expected_values[gle.account][2], gle.credit)
+		for i, gle in enumerate(gl_entries):
+			self.assertEqual(expected_values[i][0], gle.account)
+			self.assertEqual(expected_values[i][1], gle.debit)
+			self.assertEqual(expected_values[i][2], gle.credit)
 
 	def test_rounding_adjustment_3(self):
 		from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
@@ -2125,13 +2124,14 @@
 				["_Test Account Service Tax - _TC", 0.0, 240.43],
 				["_Test Account VAT - _TC", 0.0, 240.43],
 				["Sales - _TC", 0.0, 4007.15],
-				["Round Off - _TC", 0.01, 0],
+				["Round Off - _TC", 0.02, 0.01],
 			]
 		)
 
 		gl_entries = frappe.db.sql(
-			"""select account, debit, credit
+			"""select account, sum(debit) as debit, sum(credit) as credit
 			from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
+			group by account
 			order by account asc""",
 			si.name,
 			as_dict=1,
@@ -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):
@@ -3371,6 +3376,13 @@
 
 		set_advance_flag(company="_Test Company", flag=0, default_account="")
 
+	def test_sales_return_negative_rate(self):
+		si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True)
+		self.assertRaises(frappe.ValidationError, si.save)
+
+		si.items[0].rate = 10
+		si.save()
+
 
 def set_advance_flag(company, flag, default_account):
 	frappe.db.set_value(
diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js
index 1a90664..ae789b5 100644
--- a/erpnext/accounts/doctype/subscription/subscription.js
+++ b/erpnext/accounts/doctype/subscription/subscription.js
@@ -2,16 +2,16 @@
 // For license information, please see license.txt
 
 frappe.ui.form.on('Subscription', {
-	setup: function(frm) {
-		frm.set_query('party_type', function() {
+	setup: function (frm) {
+		frm.set_query('party_type', function () {
 			return {
-				filters : {
+				filters: {
 					name: ['in', ['Customer', 'Supplier']]
 				}
 			}
 		});
 
-		frm.set_query('cost_center', function() {
+		frm.set_query('cost_center', function () {
 			return {
 				filters: {
 					company: frm.doc.company
@@ -20,76 +20,60 @@
 		});
 	},
 
-	refresh: function(frm) {
-		if(!frm.is_new()){
-			if(frm.doc.status !== 'Cancelled'){
-				frm.add_custom_button(
-					__('Cancel Subscription'),
-					() => frm.events.cancel_this_subscription(frm)
-				);
-				frm.add_custom_button(
-					__('Fetch Subscription Updates'),
-					() => frm.events.get_subscription_updates(frm)
-				);
-			}
-			else if(frm.doc.status === 'Cancelled'){
-				frm.add_custom_button(
-					__('Restart Subscription'),
-					() => frm.events.renew_this_subscription(frm)
-				);
-			}
+	refresh: function (frm) {
+		if (frm.is_new()) return;
+
+		if (frm.doc.status !== 'Cancelled') {
+			frm.add_custom_button(
+				__('Fetch Subscription Updates'),
+				() => frm.trigger('get_subscription_updates'),
+				__('Actions')
+			);
+
+			frm.add_custom_button(
+				__('Cancel Subscription'),
+				() => frm.trigger('cancel_this_subscription'),
+				__('Actions')
+			);
+		} else if (frm.doc.status === 'Cancelled') {
+			frm.add_custom_button(
+				__('Restart Subscription'),
+				() => frm.trigger('renew_this_subscription'),
+				__('Actions')
+			);
 		}
 	},
 
-	cancel_this_subscription: function(frm) {
-		const doc = frm.doc;
+	cancel_this_subscription: function (frm) {
 		frappe.confirm(
 			__('This action will stop future billing. Are you sure you want to cancel this subscription?'),
-			function() {
-				frappe.call({
-					method:
-					"erpnext.accounts.doctype.subscription.subscription.cancel_subscription",
-					args: {name: doc.name},
-					callback: function(data){
-						if(!data.exc){
-							frm.reload_doc();
-						}
+			() => {
+				frm.call('cancel_subscription').then(r => {
+					if (!r.exec) {
+						frm.reload_doc();
 					}
 				});
 			}
 		);
 	},
 
-	renew_this_subscription: function(frm) {
-		const doc = frm.doc;
+	renew_this_subscription: function (frm) {
 		frappe.confirm(
-			__('You will lose records of previously generated invoices. Are you sure you want to restart this subscription?'),
-			function() {
-				frappe.call({
-					method:
-					"erpnext.accounts.doctype.subscription.subscription.restart_subscription",
-					args: {name: doc.name},
-					callback: function(data){
-						if(!data.exc){
-							frm.reload_doc();
-						}
+			__('Are you sure you want to restart this subscription?'),
+			() => {
+				frm.call('restart_subscription').then(r => {
+					if (!r.exec) {
+						frm.reload_doc();
 					}
 				});
 			}
 		);
 	},
 
-	get_subscription_updates: function(frm) {
-		const doc = frm.doc;
-		frappe.call({
-			method:
-			"erpnext.accounts.doctype.subscription.subscription.get_subscription_updates",
-			args: {name: doc.name},
-			freeze: true,
-			callback: function(data){
-				if(!data.exc){
-					frm.reload_doc();
-				}
+	get_subscription_updates: function (frm) {
+		frm.call('process').then(r => {
+			if (!r.exec) {
+				frm.reload_doc();
 			}
 		});
 	}
diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json
index c4e4be7..c15aa1e 100644
--- a/erpnext/accounts/doctype/subscription/subscription.json
+++ b/erpnext/accounts/doctype/subscription/subscription.json
@@ -19,6 +19,7 @@
   "trial_period_end",
   "follow_calendar_months",
   "generate_new_invoices_past_due_date",
+  "submit_invoice",
   "column_break_11",
   "current_invoice_start",
   "current_invoice_end",
@@ -35,12 +36,8 @@
   "cb_2",
   "additional_discount_percentage",
   "additional_discount_amount",
-  "sb_3",
-  "submit_invoice",
-  "invoices",
   "accounting_dimensions_section",
-  "cost_center",
-  "dimension_col_break"
+  "cost_center"
  ],
  "fields": [
   {
@@ -163,29 +160,12 @@
    "label": "Additional DIscount Amount"
   },
   {
-   "depends_on": "eval:doc.invoices",
-   "fieldname": "sb_3",
-   "fieldtype": "Section Break",
-   "label": "Invoices"
-  },
-  {
-   "collapsible": 1,
-   "fieldname": "invoices",
-   "fieldtype": "Table",
-   "label": "Invoices",
-   "options": "Subscription Invoice"
-  },
-  {
    "collapsible": 1,
    "fieldname": "accounting_dimensions_section",
    "fieldtype": "Section Break",
    "label": "Accounting Dimensions"
   },
   {
-   "fieldname": "dimension_col_break",
-   "fieldtype": "Column Break"
-  },
-  {
    "fieldname": "party_type",
    "fieldtype": "Link",
    "label": "Party Type",
@@ -259,15 +239,27 @@
    "default": "1",
    "fieldname": "submit_invoice",
    "fieldtype": "Check",
-   "label": "Submit Invoice Automatically"
+   "label": "Submit Generated Invoices"
   }
  ],
  "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-04-19 15:24:27.550797",
+ "links": [
+  {
+   "group": "Buying",
+   "link_doctype": "Purchase Invoice",
+   "link_fieldname": "subscription"
+  },
+  {
+   "group": "Selling",
+   "link_doctype": "Sales Invoice",
+   "link_fieldname": "subscription"
+  }
+ ],
+ "modified": "2022-02-18 23:24:57.185054",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Subscription",
+ "naming_rule": "Expression (old style)",
  "owner": "Administrator",
  "permissions": [
   {
@@ -309,5 +301,6 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 8708342..bbcade1 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -2,14 +2,17 @@
 # For license information, please see license.txt
 
 
+from datetime import datetime
+from typing import Dict, List, Optional, Union
+
 import frappe
 from frappe import _
 from frappe.model.document import Document
 from frappe.utils.data import (
 	add_days,
+	add_months,
 	add_to_date,
 	cint,
-	cstr,
 	date_diff,
 	flt,
 	get_last_day,
@@ -17,8 +20,7 @@
 	nowdate,
 )
 
-import erpnext
-from erpnext import get_default_company
+from erpnext import get_default_company, get_default_cost_center
 from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
 	get_accounting_dimensions,
 )
@@ -26,33 +28,39 @@
 from erpnext.accounts.party import get_party_account_currency
 
 
+class InvoiceCancelled(frappe.ValidationError):
+	pass
+
+
+class InvoiceNotCancelled(frappe.ValidationError):
+	pass
+
+
 class Subscription(Document):
 	def before_insert(self):
 		# update start just before the subscription doc is created
 		self.update_subscription_period(self.start_date)
 
-	def update_subscription_period(self, date=None, return_date=False):
+	def update_subscription_period(self, date: Optional[Union[datetime.date, str]] = None):
 		"""
 		Subscription period is the period to be billed. This method updates the
 		beginning of the billing period and end of the billing period.
-
 		The beginning of the billing period is represented in the doctype as
 		`current_invoice_start` and the end of the billing period is represented
 		as `current_invoice_end`.
-
-		If return_date is True, it wont update the start and end dates.
-		This is implemented to get the dates to check if is_current_invoice_generated
 		"""
+		self.current_invoice_start = self.get_current_invoice_start(date)
+		self.current_invoice_end = self.get_current_invoice_end(self.current_invoice_start)
+
+	def _get_subscription_period(self, date: Optional[Union[datetime.date, str]] = None):
 		_current_invoice_start = self.get_current_invoice_start(date)
 		_current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
 
-		if return_date:
-			return _current_invoice_start, _current_invoice_end
+		return _current_invoice_start, _current_invoice_end
 
-		self.current_invoice_start = _current_invoice_start
-		self.current_invoice_end = _current_invoice_end
-
-	def get_current_invoice_start(self, date=None):
+	def get_current_invoice_start(
+		self, date: Optional[Union[datetime.date, str]] = None
+	) -> Union[datetime.date, str]:
 		"""
 		This returns the date of the beginning of the current billing period.
 		If the `date` parameter is not given , it will be automatically set as today's
@@ -75,13 +83,13 @@
 
 		return _current_invoice_start
 
-	def get_current_invoice_end(self, date=None):
+	def get_current_invoice_end(
+		self, date: Optional[Union[datetime.date, str]] = None
+	) -> Union[datetime.date, str]:
 		"""
 		This returns the date of the end of the current billing period.
-
 		If the subscription is in trial period, it will be set as the end of the
 		trial period.
-
 		If is not in a trial period, it will be `x` days from the beginning of the
 		current billing period where `x` is the billing interval from the
 		`Subscription Plan` in the `Subscription`.
@@ -105,24 +113,13 @@
 				_current_invoice_end = get_last_day(date)
 
 			if self.follow_calendar_months:
+				# Sets the end date
+				# eg if date is 17-Feb-2022, the invoice will be generated per month ie
+				# the invoice will be created from 17 Feb to 28 Feb
 				billing_info = self.get_billing_cycle_and_interval()
 				billing_interval_count = billing_info[0]["billing_interval_count"]
-				calendar_months = get_calendar_months(billing_interval_count)
-				calendar_month = 0
-				current_invoice_end_month = getdate(_current_invoice_end).month
-				current_invoice_end_year = getdate(_current_invoice_end).year
-
-				for month in calendar_months:
-					if month <= current_invoice_end_month:
-						calendar_month = month
-
-				if cint(calendar_month - billing_interval_count) <= 0 and getdate(date).month != 1:
-					calendar_month = 12
-					current_invoice_end_year -= 1
-
-				_current_invoice_end = get_last_day(
-					cstr(current_invoice_end_year) + "-" + cstr(calendar_month) + "-01"
-				)
+				_end = add_months(getdate(date), billing_interval_count - 1)
+				_current_invoice_end = get_last_day(_end)
 
 			if self.end_date and getdate(_current_invoice_end) > getdate(self.end_date):
 				_current_invoice_end = self.end_date
@@ -130,7 +127,7 @@
 		return _current_invoice_end
 
 	@staticmethod
-	def validate_plans_billing_cycle(billing_cycle_data):
+	def validate_plans_billing_cycle(billing_cycle_data: List[Dict[str, str]]) -> None:
 		"""
 		Makes sure that all `Subscription Plan` in the `Subscription` have the
 		same billing interval
@@ -138,10 +135,9 @@
 		if billing_cycle_data and len(billing_cycle_data) != 1:
 			frappe.throw(_("You can only have Plans with the same billing cycle in a Subscription"))
 
-	def get_billing_cycle_and_interval(self):
+	def get_billing_cycle_and_interval(self) -> List[Dict[str, str]]:
 		"""
 		Returns a dict representing the billing interval and cycle for this `Subscription`.
-
 		You shouldn't need to call this directly. Use `get_billing_cycle` instead.
 		"""
 		plan_names = [plan.plan for plan in self.plans]
@@ -156,72 +152,65 @@
 
 		return billing_info
 
-	def get_billing_cycle_data(self):
+	def get_billing_cycle_data(self) -> Dict[str, int]:
 		"""
 		Returns dict contain the billing cycle data.
-
 		You shouldn't need to call this directly. Use `get_billing_cycle` instead.
 		"""
 		billing_info = self.get_billing_cycle_and_interval()
+		if not billing_info:
+			return None
 
-		self.validate_plans_billing_cycle(billing_info)
+		data = dict()
+		interval = billing_info[0]["billing_interval"]
+		interval_count = billing_info[0]["billing_interval_count"]
 
-		if billing_info:
-			data = dict()
-			interval = billing_info[0]["billing_interval"]
-			interval_count = billing_info[0]["billing_interval_count"]
-			if interval not in ["Day", "Week"]:
-				data["days"] = -1
-			if interval == "Day":
-				data["days"] = interval_count - 1
-			elif interval == "Month":
-				data["months"] = interval_count
-			elif interval == "Year":
-				data["years"] = interval_count
-			# todo: test week
-			elif interval == "Week":
-				data["days"] = interval_count * 7 - 1
+		if interval not in ["Day", "Week"]:
+			data["days"] = -1
 
-			return data
+		if interval == "Day":
+			data["days"] = interval_count - 1
+		elif interval == "Week":
+			data["days"] = interval_count * 7 - 1
+		elif interval == "Month":
+			data["months"] = interval_count
+		elif interval == "Year":
+			data["years"] = interval_count
 
-	def set_status_grace_period(self):
-		"""
-		Sets the `Subscription` `status` based on the preference set in `Subscription Settings`.
+		return data
 
-		Used when the `Subscription` needs to decide what to do after the current generated
-		invoice is past it's due date and grace period.
-		"""
-		subscription_settings = frappe.get_single("Subscription Settings")
-		if self.status == "Past Due Date" and self.is_past_grace_period():
-			self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid"
-
-	def set_subscription_status(self):
+	def set_subscription_status(self) -> None:
 		"""
 		Sets the status of the `Subscription`
 		"""
 		if self.is_trialling():
 			self.status = "Trialling"
-		elif self.status == "Active" and self.end_date and getdate() > getdate(self.end_date):
+		elif (
+			self.status == "Active"
+			and self.end_date
+			and getdate(frappe.flags.current_date) > getdate(self.end_date)
+		):
 			self.status = "Completed"
 		elif self.is_past_grace_period():
-			subscription_settings = frappe.get_single("Subscription Settings")
-			self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid"
+			self.status = self.get_status_for_past_grace_period()
+			self.cancelation_date = (
+				getdate(frappe.flags.current_date) if self.status == "Cancelled" else None
+			)
 		elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
 			self.status = "Past Due Date"
-		elif not self.has_outstanding_invoice():
+		elif not self.has_outstanding_invoice() or self.is_new_subscription():
 			self.status = "Active"
-		elif self.is_new_subscription():
-			self.status = "Active"
+
 		self.save()
 
-	def is_trialling(self):
+	def is_trialling(self) -> bool:
 		"""
 		Returns `True` if the `Subscription` is in trial period.
 		"""
 		return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
 
 	@staticmethod
-	def period_has_passed(end_date):
+	def period_has_passed(end_date: Union[str, datetime.date]) -> bool:
 		"""
 		Returns true if the given `end_date` has passed
 		"""
@@ -229,61 +218,59 @@
 		if not end_date:
 			return True
 
-		end_date = getdate(end_date)
-		return getdate() > getdate(end_date)
+		return getdate(frappe.flags.current_date) > getdate(end_date)
 
-	def is_past_grace_period(self):
+	def get_status_for_past_grace_period(self) -> str:
+		cancel_after_grace = cint(frappe.get_value("Subscription Settings", None, "cancel_after_grace"))
+		status = "Unpaid"
+
+		if cancel_after_grace:
+			status = "Cancelled"
+
+		return status
+
+	def is_past_grace_period(self) -> bool:
 		"""
 		Returns `True` if the grace period for the `Subscription` has passed
 		"""
-		current_invoice = self.get_current_invoice()
-		if self.current_invoice_is_past_due(current_invoice):
-			subscription_settings = frappe.get_single("Subscription Settings")
-			grace_period = cint(subscription_settings.grace_period)
+		if not self.current_invoice_is_past_due():
+			return
 
-			return getdate() > add_days(current_invoice.due_date, grace_period)
+		grace_period = cint(frappe.get_value("Subscription Settings", None, "grace_period"))
+		return getdate(frappe.flags.current_date) >= getdate(
+			add_days(self.current_invoice.due_date, grace_period)
+		)
 
-	def current_invoice_is_past_due(self, current_invoice=None):
+	def current_invoice_is_past_due(self) -> bool:
 		"""
 		Returns `True` if the current generated invoice is overdue
 		"""
-		if not current_invoice:
-			current_invoice = self.get_current_invoice()
-
-		if not current_invoice or self.is_paid(current_invoice):
+		if not self.current_invoice or self.is_paid(self.current_invoice):
 			return False
-		else:
-			return getdate() > getdate(current_invoice.due_date)
 
-	def get_current_invoice(self):
-		"""
-		Returns the most recent generated invoice.
-		"""
-		doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
+		return getdate(frappe.flags.current_date) >= getdate(self.current_invoice.due_date)
 
-		if len(self.invoices):
-			current = self.invoices[-1]
-			if frappe.db.exists(doctype, current.get("invoice")):
-				doc = frappe.get_doc(doctype, current.get("invoice"))
-				return doc
-			else:
-				frappe.throw(_("Invoice {0} no longer exists").format(current.get("invoice")))
+	@property
+	def invoice_document_type(self) -> str:
+		return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
 
-	def is_new_subscription(self):
+	def is_new_subscription(self) -> bool:
 		"""
 		Returns `True` if `Subscription` has never generated an invoice
 		"""
-		return len(self.invoices) == 0
+		return self.is_new() or not frappe.db.exists(
+			{"doctype": self.invoice_document_type, "subscription": self.name}
+		)
 
-	def validate(self):
+	def validate(self) -> None:
 		self.validate_trial_period()
 		self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
 		self.validate_end_date()
 		self.validate_to_follow_calendar_months()
 		if not self.cost_center:
-			self.cost_center = erpnext.get_default_cost_center(self.get("company"))
+			self.cost_center = get_default_cost_center(self.get("company"))
 
-	def validate_trial_period(self):
+	def validate_trial_period(self) -> None:
 		"""
 		Runs sanity checks on trial period dates for the `Subscription`
 		"""
@@ -297,7 +284,7 @@
 		if self.trial_period_start and getdate(self.trial_period_start) > getdate(self.start_date):
 			frappe.throw(_("Trial Period Start date cannot be after Subscription Start Date"))
 
-	def validate_end_date(self):
+	def validate_end_date(self) -> None:
 		billing_cycle_info = self.get_billing_cycle_data()
 		end_date = add_to_date(self.start_date, **billing_cycle_info)
 
@@ -306,53 +293,53 @@
 				_("Subscription End Date must be after {0} as per the subscription plan").format(end_date)
 			)
 
-	def validate_to_follow_calendar_months(self):
-		if self.follow_calendar_months:
-			billing_info = self.get_billing_cycle_and_interval()
+	def validate_to_follow_calendar_months(self) -> None:
+		if not self.follow_calendar_months:
+			return
 
-			if not self.end_date:
-				frappe.throw(_("Subscription End Date is mandatory to follow calendar months"))
+		billing_info = self.get_billing_cycle_and_interval()
 
-			if billing_info[0]["billing_interval"] != "Month":
-				frappe.throw(
-					_("Billing Interval in Subscription Plan must be Month to follow calendar months")
-				)
+		if not self.end_date:
+			frappe.throw(_("Subscription End Date is mandatory to follow calendar months"))
 
-	def after_insert(self):
+		if billing_info[0]["billing_interval"] != "Month":
+			frappe.throw(_("Billing Interval in Subscription Plan must be Month to follow calendar months"))
+
+	def after_insert(self) -> None:
 		# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
 		self.set_subscription_status()
 
-	def generate_invoice(self, prorate=0):
+	def generate_invoice(
+		self,
+		from_date: Optional[Union[str, datetime.date]] = None,
+		to_date: Optional[Union[str, datetime.date]] = None,
+	) -> Document:
 		"""
 		Creates a `Invoice` for the `Subscription`, updates `self.invoices` and
 		saves the `Subscription`.
+		Backwards compatibility
 		"""
+		return self.create_invoice(from_date=from_date, to_date=to_date)
 
-		doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
-
-		invoice = self.create_invoice(prorate)
-		self.append("invoices", {"document_type": doctype, "invoice": invoice.name})
-
-		self.save()
-
-		return invoice
-
-	def create_invoice(self, prorate):
+	def create_invoice(
+		self,
+		from_date: Optional[Union[str, datetime.date]] = None,
+		to_date: Optional[Union[str, datetime.date]] = None,
+	) -> Document:
 		"""
 		Creates a `Invoice`, submits it and returns it
 		"""
-		doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
-
-		invoice = frappe.new_doc(doctype)
-
 		# For backward compatibility
 		# Earlier subscription didn't had any company field
 		company = self.get("company") or get_default_company()
 		if not company:
+			# fmt: off
 			frappe.throw(
-				_("Company is mandatory was generating invoice. Please set default company in Global Defaults")
+				_("Company is mandatory was generating invoice. Please set default company in Global Defaults.")
 			)
+			# fmt: on
 
+		invoice = frappe.new_doc(self.invoice_document_type)
 		invoice.company = company
 		invoice.set_posting_time = 1
 		invoice.posting_date = (
@@ -363,17 +350,17 @@
 
 		invoice.cost_center = self.cost_center
 
-		if doctype == "Sales Invoice":
+		if self.invoice_document_type == "Sales Invoice":
 			invoice.customer = self.party
 		else:
 			invoice.supplier = self.party
 			if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"):
 				invoice.apply_tds = 1
 
-		### Add party currency to invoice
+		# Add party currency to invoice
 		invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
 
-		## Add dimensions in invoice for subscription:
+		# Add dimensions in invoice for subscription:
 		accounting_dimensions = get_accounting_dimensions()
 
 		for dimension in accounting_dimensions:
@@ -382,7 +369,7 @@
 
 		# Subscription is better suited for service items. I won't update `update_stock`
 		# for that reason
-		items_list = self.get_items_from_plans(self.plans, prorate)
+		items_list = self.get_items_from_plans(self.plans, is_prorate())
 		for item in items_list:
 			item["cost_center"] = self.cost_center
 			invoice.append("items", item)
@@ -390,9 +377,9 @@
 		# Taxes
 		tax_template = ""
 
-		if doctype == "Sales Invoice" and self.sales_tax_template:
+		if self.invoice_document_type == "Sales Invoice" and self.sales_tax_template:
 			tax_template = self.sales_tax_template
-		if doctype == "Purchase Invoice" and self.purchase_tax_template:
+		if self.invoice_document_type == "Purchase Invoice" and self.purchase_tax_template:
 			tax_template = self.purchase_tax_template
 
 		if tax_template:
@@ -424,8 +411,9 @@
 				invoice.apply_discount_on = discount_on if discount_on else "Grand Total"
 
 		# Subscription period
-		invoice.from_date = self.current_invoice_start
-		invoice.to_date = self.current_invoice_end
+		invoice.subscription = self.name
+		invoice.from_date = from_date or self.current_invoice_start
+		invoice.to_date = to_date or self.current_invoice_end
 
 		invoice.flags.ignore_mandatory = True
 
@@ -437,13 +425,20 @@
 
 		return invoice
 
-	def get_items_from_plans(self, plans, prorate=0):
+	def get_items_from_plans(
+		self, plans: List[Dict[str, str]], prorate: Optional[bool] = None
+	) -> List[Dict]:
 		"""
 		Returns the `Item`s linked to `Subscription Plan`
 		"""
+		if prorate is None:
+			prorate = False
+
 		if prorate:
 			prorate_factor = get_prorata_factor(
-				self.current_invoice_end, self.current_invoice_start, self.generate_invoice_at_period_start
+				self.current_invoice_end,
+				self.current_invoice_start,
+				cint(self.generate_invoice_at_period_start),
 			)
 
 		items = []
@@ -465,7 +460,11 @@
 					"item_code": item_code,
 					"qty": plan.qty,
 					"rate": get_plan_rate(
-						plan.plan, plan.qty, party, self.current_invoice_start, self.current_invoice_end
+						plan.plan,
+						plan.qty,
+						party,
+						self.current_invoice_start,
+						self.current_invoice_end,
 					),
 					"cost_center": plan_doc.cost_center,
 				}
@@ -503,254 +502,184 @@
 
 		return items
 
-	def process(self):
+	@frappe.whitelist()
+	def process(self) -> bool:
 		"""
 		To be called by task periodically. It checks the subscription and takes appropriate action
 		as need be. It calls either of these methods depending the `Subscription` status:
 		1. `process_for_active`
 		2. `process_for_past_due`
 		"""
-		if self.status == "Active":
-			self.process_for_active()
-		elif self.status in ["Past Due Date", "Unpaid"]:
-			self.process_for_past_due_date()
+		if (
+			not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end)
+			and self.can_generate_new_invoice()
+		):
+			self.generate_invoice()
+			self.update_subscription_period(add_days(self.current_invoice_end, 1))
+
+		if self.cancel_at_period_end and (
+			getdate(frappe.flags.current_date) >= getdate(self.current_invoice_end)
+			or getdate(frappe.flags.current_date) >= getdate(self.end_date)
+		):
+			self.cancel_subscription()
 
 		self.set_subscription_status()
 
 		self.save()
 
-	def is_postpaid_to_invoice(self):
-		return getdate() > getdate(self.current_invoice_end) or (
-			getdate() >= getdate(self.current_invoice_end)
-			and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)
-		)
+	def can_generate_new_invoice(self) -> bool:
+		if self.cancelation_date:
+			return False
+		elif self.generate_invoice_at_period_start and (
+			getdate(frappe.flags.current_date) == getdate(self.current_invoice_start)
+			or self.is_new_subscription()
+		):
+			return True
+		elif getdate(frappe.flags.current_date) == getdate(self.current_invoice_end):
+			if self.has_outstanding_invoice() and not self.generate_new_invoices_past_due_date:
+				return False
 
-	def is_prepaid_to_invoice(self):
-		if not self.generate_invoice_at_period_start:
+			return True
+		else:
 			return False
 
-		if self.is_new_subscription() and getdate() >= getdate(self.current_invoice_start):
-			return True
-
-		# Check invoice dates and make sure it doesn't have outstanding invoices
-		return getdate() >= getdate(self.current_invoice_start)
-
-	def is_current_invoice_generated(self, _current_start_date=None, _current_end_date=None):
-		invoice = self.get_current_invoice()
-
+	def is_current_invoice_generated(
+		self,
+		_current_start_date: Union[datetime.date, str] = None,
+		_current_end_date: Union[datetime.date, str] = None,
+	) -> bool:
 		if not (_current_start_date and _current_end_date):
-			_current_start_date, _current_end_date = self.update_subscription_period(
-				date=add_days(self.current_invoice_end, 1), return_date=True
+			_current_start_date, _current_end_date = self._get_subscription_period(
+				date=add_days(self.current_invoice_end, 1)
 			)
 
-		if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(
-			_current_end_date
-		):
+		if self.current_invoice and getdate(_current_start_date) <= getdate(
+			self.current_invoice.posting_date
+		) <= getdate(_current_end_date):
 			return True
 
 		return False
 
-	def process_for_active(self):
+	@property
+	def current_invoice(self) -> Union[Document, None]:
 		"""
-		Called by `process` if the status of the `Subscription` is 'Active'.
-
-		The possible outcomes of this method are:
-		1. Generate a new invoice
-		2. Change the `Subscription` status to 'Past Due Date'
-		3. Change the `Subscription` status to 'Cancelled'
+		Adds property for accessing the current_invoice
 		"""
+		return self.get_current_invoice()
 
-		if not self.is_current_invoice_generated(
-			self.current_invoice_start, self.current_invoice_end
-		) and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
+	def get_current_invoice(self) -> Union[Document, None]:
+		"""
+		Returns the most recent generated invoice.
+		"""
+		invoice = frappe.get_all(
+			self.invoice_document_type,
+			{
+				"subscription": self.name,
+			},
+			limit=1,
+			order_by="to_date desc",
+			pluck="name",
+		)
 
-			prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
-			self.generate_invoice(prorate)
+		if invoice:
+			return frappe.get_doc(self.invoice_document_type, invoice[0])
 
-		if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
-			self.update_subscription_period(add_days(self.current_invoice_end, 1))
-
-		if self.cancel_at_period_end and getdate() > getdate(self.current_invoice_end):
-			self.cancel_subscription_at_period_end()
-
-	def cancel_subscription_at_period_end(self):
+	def cancel_subscription_at_period_end(self) -> None:
 		"""
 		Called when `Subscription.cancel_at_period_end` is truthy
 		"""
-		if self.end_date and getdate() < getdate(self.end_date):
-			return
-
 		self.status = "Cancelled"
-		if not self.cancelation_date:
-			self.cancelation_date = nowdate()
+		self.cancelation_date = nowdate()
 
-	def process_for_past_due_date(self):
-		"""
-		Called by `process` if the status of the `Subscription` is 'Past Due Date'.
-
-		The possible outcomes of this method are:
-		1. Change the `Subscription` status to 'Active'
-		2. Change the `Subscription` status to 'Cancelled'
-		3. Change the `Subscription` status to 'Unpaid'
-		"""
-		current_invoice = self.get_current_invoice()
-		if not current_invoice:
-			frappe.throw(_("Current invoice {0} is missing").format(current_invoice.invoice))
-		else:
-			if not self.has_outstanding_invoice():
-				self.status = "Active"
-			else:
-				self.set_status_grace_period()
-
-			if getdate() > getdate(self.current_invoice_end):
-				self.update_subscription_period(add_days(self.current_invoice_end, 1))
-
-			# Generate invoices periodically even if current invoice are unpaid
-			if (
-				self.generate_new_invoices_past_due_date
-				and not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end)
-				and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice())
-			):
-
-				prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
-				self.generate_invoice(prorate)
+	@property
+	def invoices(self) -> List[Dict]:
+		return frappe.get_all(
+			self.invoice_document_type,
+			filters={"subscription": self.name},
+			order_by="from_date asc",
+		)
 
 	@staticmethod
-	def is_paid(invoice):
+	def is_paid(invoice: Document) -> bool:
 		"""
 		Return `True` if the given invoice is paid
 		"""
 		return invoice.status == "Paid"
 
-	def has_outstanding_invoice(self):
+	def has_outstanding_invoice(self) -> int:
 		"""
 		Returns `True` if the most recent invoice for the `Subscription` is not paid
 		"""
-		doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
-		current_invoice = self.get_current_invoice()
-		invoice_list = [d.invoice for d in self.invoices]
-
-		outstanding_invoices = frappe.get_all(
-			doctype, fields=["name"], filters={"status": ("!=", "Paid"), "name": ("in", invoice_list)}
+		return frappe.db.count(
+			self.invoice_document_type,
+			{
+				"subscription": self.name,
+				"status": ["!=", "Paid"],
+			},
 		)
 
-		if outstanding_invoices:
-			return True
-		else:
-			False
-
-	def cancel_subscription(self):
+	@frappe.whitelist()
+	def cancel_subscription(self) -> None:
 		"""
 		This sets the subscription as cancelled. It will stop invoices from being generated
 		but it will not affect already created invoices.
 		"""
-		if self.status != "Cancelled":
-			to_generate_invoice = (
-				True if self.status == "Active" and not self.generate_invoice_at_period_start else False
-			)
-			to_prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
-			self.status = "Cancelled"
-			self.cancelation_date = nowdate()
-			if to_generate_invoice:
-				self.generate_invoice(prorate=to_prorate)
-			self.save()
+		if self.status == "Cancelled":
+			frappe.throw(_("subscription is already cancelled."), InvoiceCancelled)
 
-	def restart_subscription(self):
+		to_generate_invoice = (
+			True if self.status == "Active" and not self.generate_invoice_at_period_start else False
+		)
+		self.status = "Cancelled"
+		self.cancelation_date = nowdate()
+
+		if to_generate_invoice:
+			self.generate_invoice(self.current_invoice_start, self.cancelation_date)
+
+		self.save()
+
+	@frappe.whitelist()
+	def restart_subscription(self) -> None:
 		"""
 		This sets the subscription as active. The subscription will be made to be like a new
 		subscription and the `Subscription` will lose all the history of generated invoices
 		it has.
 		"""
-		if self.status == "Cancelled":
-			self.status = "Active"
-			self.db_set("start_date", nowdate())
-			self.update_subscription_period(nowdate())
-			self.invoices = []
-			self.save()
-		else:
-			frappe.throw(_("You cannot restart a Subscription that is not cancelled."))
+		if not self.status == "Cancelled":
+			frappe.throw(_("You cannot restart a Subscription that is not cancelled."), InvoiceNotCancelled)
 
-	def get_precision(self):
-		invoice = self.get_current_invoice()
-		if invoice:
-			return invoice.precision("grand_total")
+		self.status = "Active"
+		self.cancelation_date = None
+		self.update_subscription_period(frappe.flags.current_date or nowdate())
+		self.save()
 
 
-def get_calendar_months(billing_interval):
-	calendar_months = []
-	start = 0
-	while start < 12:
-		start += billing_interval
-		calendar_months.append(start)
-
-	return calendar_months
+def is_prorate() -> int:
+	return cint(frappe.db.get_single_value("Subscription Settings", "prorate"))
 
 
-def get_prorata_factor(period_end, period_start, is_prepaid):
+def get_prorata_factor(
+	period_end: Union[datetime.date, str],
+	period_start: Union[datetime.date, str],
+	is_prepaid: Optional[int] = None,
+) -> Union[int, float]:
 	if is_prepaid:
-		prorate_factor = 1
-	else:
-		diff = flt(date_diff(nowdate(), period_start) + 1)
-		plan_days = flt(date_diff(period_end, period_start) + 1)
-		prorate_factor = diff / plan_days
+		return 1
 
-	return prorate_factor
+	diff = flt(date_diff(nowdate(), period_start) + 1)
+	plan_days = flt(date_diff(period_end, period_start) + 1)
+	return diff / plan_days
 
 
-def process_all():
+def process_all() -> None:
 	"""
 	Task to updates the status of all `Subscription` apart from those that are cancelled
 	"""
-	subscriptions = get_all_subscriptions()
-	for subscription in subscriptions:
-		process(subscription)
-
-
-def get_all_subscriptions():
-	"""
-	Returns all `Subscription` documents
-	"""
-	return frappe.db.get_all("Subscription", {"status": ("!=", "Cancelled")})
-
-
-def process(data):
-	"""
-	Checks a `Subscription` and updates it status as necessary
-	"""
-	if data:
+	for subscription in frappe.get_all("Subscription", {"status": ("!=", "Cancelled")}, pluck="name"):
 		try:
-			subscription = frappe.get_doc("Subscription", data["name"])
+			subscription = frappe.get_doc("Subscription", subscription)
 			subscription.process()
 			frappe.db.commit()
 		except frappe.ValidationError:
 			frappe.db.rollback()
 			subscription.log_error("Subscription failed")
-
-
-@frappe.whitelist()
-def cancel_subscription(name):
-	"""
-	Cancels a `Subscription`. This will stop the `Subscription` from further invoicing the
-	`Subscriber` but all already outstanding invoices will not be affected.
-	"""
-	subscription = frappe.get_doc("Subscription", name)
-	subscription.cancel_subscription()
-
-
-@frappe.whitelist()
-def restart_subscription(name):
-	"""
-	Restarts a cancelled `Subscription`. The `Subscription` will 'forget' the history of
-	all invoices it has generated
-	"""
-	subscription = frappe.get_doc("Subscription", name)
-	subscription.restart_subscription()
-
-
-@frappe.whitelist()
-def get_subscription_updates(name):
-	"""
-	Use this to get the latest state of the given `Subscription`
-	"""
-	subscription = frappe.get_doc("Subscription", name)
-	subscription.process()
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index eb17daa..0bb171f 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -11,6 +11,7 @@
 	date_diff,
 	flt,
 	get_date_str,
+	getdate,
 	nowdate,
 )
 
@@ -90,10 +91,18 @@
 		customer.insert()
 
 
+def reset_settings():
+	settings = frappe.get_single("Subscription Settings")
+	settings.grace_period = 0
+	settings.cancel_after_grace = 0
+	settings.save()
+
+
 class TestSubscription(unittest.TestCase):
 	def setUp(self):
 		create_plan()
 		create_parties()
+		reset_settings()
 
 	def test_create_subscription_with_trial_with_correct_period(self):
 		subscription = frappe.new_doc("Subscription")
@@ -116,8 +125,6 @@
 		self.assertEqual(subscription.invoices, [])
 		self.assertEqual(subscription.status, "Trialling")
 
-		subscription.delete()
-
 	def test_create_subscription_without_trial_with_correct_period(self):
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Customer"
@@ -133,8 +140,6 @@
 		self.assertEqual(len(subscription.invoices), 0)
 		self.assertEqual(subscription.status, "Active")
 
-		subscription.delete()
-
 	def test_create_subscription_trial_with_wrong_dates(self):
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Customer"
@@ -144,7 +149,6 @@
 		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 
 		self.assertRaises(frappe.ValidationError, subscription.save)
-		subscription.delete()
 
 	def test_create_subscription_multi_with_different_billing_fails(self):
 		subscription = frappe.new_doc("Subscription")
@@ -156,7 +160,6 @@
 		subscription.append("plans", {"plan": "_Test Plan Name 3", "qty": 1})
 
 		self.assertRaises(frappe.ValidationError, subscription.save)
-		subscription.delete()
 
 	def test_invoice_is_generated_at_end_of_billing_period(self):
 		subscription = frappe.new_doc("Subscription")
@@ -169,13 +172,13 @@
 		self.assertEqual(subscription.status, "Active")
 		self.assertEqual(subscription.current_invoice_start, "2018-01-01")
 		self.assertEqual(subscription.current_invoice_end, "2018-01-31")
+		frappe.flags.current_date = "2018-01-31"
 		subscription.process()
 
 		self.assertEqual(len(subscription.invoices), 1)
-		self.assertEqual(subscription.current_invoice_start, "2018-01-01")
-		subscription.process()
+		self.assertEqual(subscription.current_invoice_start, "2018-02-01")
+		self.assertEqual(subscription.current_invoice_end, "2018-02-28")
 		self.assertEqual(subscription.status, "Unpaid")
-		subscription.delete()
 
 	def test_status_goes_back_to_active_after_invoice_is_paid(self):
 		subscription = frappe.new_doc("Subscription")
@@ -183,7 +186,9 @@
 		subscription.party = "_Test Customer"
 		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 		subscription.start_date = "2018-01-01"
+		subscription.generate_invoice_at_period_start = True
 		subscription.insert()
+		frappe.flags.current_date = "2018-01-01"
 		subscription.process()  # generate first invoice
 		self.assertEqual(len(subscription.invoices), 1)
 
@@ -203,11 +208,8 @@
 		self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1))
 		self.assertEqual(len(subscription.invoices), 1)
 
-		subscription.delete()
-
 	def test_subscription_cancel_after_grace_period(self):
 		settings = frappe.get_single("Subscription Settings")
-		default_grace_period_action = settings.cancel_after_grace
 		settings.cancel_after_grace = 1
 		settings.save()
 
@@ -215,20 +217,18 @@
 		subscription.party_type = "Customer"
 		subscription.party = "_Test Customer"
 		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+		# subscription.generate_invoice_at_period_start = True
 		subscription.start_date = "2018-01-01"
 		subscription.insert()
 
 		self.assertEqual(subscription.status, "Active")
 
+		frappe.flags.current_date = "2018-01-31"
 		subscription.process()  # generate first invoice
 		# This should change status to Cancelled since grace period is 0
 		# And is backdated subscription so subscription will be cancelled after processing
 		self.assertEqual(subscription.status, "Cancelled")
 
-		settings.cancel_after_grace = default_grace_period_action
-		settings.save()
-		subscription.delete()
-
 	def test_subscription_unpaid_after_grace_period(self):
 		settings = frappe.get_single("Subscription Settings")
 		default_grace_period_action = settings.cancel_after_grace
@@ -248,21 +248,26 @@
 
 		settings.cancel_after_grace = default_grace_period_action
 		settings.save()
-		subscription.delete()
 
 	def test_subscription_invoice_days_until_due(self):
+		_date = add_months(nowdate(), -1)
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Customer"
 		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 		subscription.days_until_due = 10
-		subscription.start_date = add_months(nowdate(), -1)
+		subscription.start_date = _date
+		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 		subscription.insert()
+
+		frappe.flags.current_date = subscription.current_invoice_end
+
 		subscription.process()  # generate first invoice
 		self.assertEqual(len(subscription.invoices), 1)
 		self.assertEqual(subscription.status, "Active")
 
-		subscription.delete()
+		frappe.flags.current_date = add_days(subscription.current_invoice_end, 3)
+		self.assertEqual(len(subscription.invoices), 1)
+		self.assertEqual(subscription.status, "Active")
 
 	def test_subscription_is_past_due_doesnt_change_within_grace_period(self):
 		settings = frappe.get_single("Subscription Settings")
@@ -276,6 +281,8 @@
 		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 		subscription.start_date = add_days(nowdate(), -1000)
 		subscription.insert()
+
+		frappe.flags.current_date = subscription.current_invoice_end
 		subscription.process()  # generate first invoice
 
 		self.assertEqual(subscription.status, "Past Due Date")
@@ -292,7 +299,6 @@
 
 		settings.grace_period = grace_period
 		settings.save()
-		subscription.delete()
 
 	def test_subscription_remains_active_during_invoice_period(self):
 		subscription = frappe.new_doc("Subscription")
@@ -319,8 +325,6 @@
 		self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
 		self.assertEqual(len(subscription.invoices), 0)
 
-		subscription.delete()
-
 	def test_subscription_cancelation(self):
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Customer"
@@ -331,8 +335,6 @@
 
 		self.assertEqual(subscription.status, "Cancelled")
 
-		subscription.delete()
-
 	def test_subscription_cancellation_invoices(self):
 		settings = frappe.get_single("Subscription Settings")
 		to_prorate = settings.prorate
@@ -372,7 +374,6 @@
 		self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2))
 		self.assertEqual(subscription.status, "Cancelled")
 
-		subscription.delete()
 		settings.prorate = to_prorate
 		settings.save()
 
@@ -395,8 +396,6 @@
 		settings.prorate = to_prorate
 		settings.save()
 
-		subscription.delete()
-
 	def test_subscription_cancellation_invoices_with_prorata_true(self):
 		settings = frappe.get_single("Subscription Settings")
 		to_prorate = settings.prorate
@@ -422,8 +421,6 @@
 		settings.prorate = to_prorate
 		settings.save()
 
-		subscription.delete()
-
 	def test_subcription_cancellation_and_process(self):
 		settings = frappe.get_single("Subscription Settings")
 		default_grace_period_action = settings.cancel_after_grace
@@ -437,23 +434,22 @@
 		subscription.start_date = "2018-01-01"
 		subscription.insert()
 		subscription.process()  # generate first invoice
-		invoices = len(subscription.invoices)
 
+		# Generate an invoice for the cancelled period
 		subscription.cancel_subscription()
 		self.assertEqual(subscription.status, "Cancelled")
-		self.assertEqual(len(subscription.invoices), invoices)
+		self.assertEqual(len(subscription.invoices), 1)
 
 		subscription.process()
 		self.assertEqual(subscription.status, "Cancelled")
-		self.assertEqual(len(subscription.invoices), invoices)
+		self.assertEqual(len(subscription.invoices), 1)
 
 		subscription.process()
 		self.assertEqual(subscription.status, "Cancelled")
-		self.assertEqual(len(subscription.invoices), invoices)
+		self.assertEqual(len(subscription.invoices), 1)
 
 		settings.cancel_after_grace = default_grace_period_action
 		settings.save()
-		subscription.delete()
 
 	def test_subscription_restart_and_process(self):
 		settings = frappe.get_single("Subscription Settings")
@@ -468,6 +464,7 @@
 		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 		subscription.start_date = "2018-01-01"
 		subscription.insert()
+		frappe.flags.current_date = "2018-01-31"
 		subscription.process()  # generate first invoice
 
 		# Status is unpaid as Days until Due is zero and grace period is Zero
@@ -478,19 +475,18 @@
 
 		subscription.restart_subscription()
 		self.assertEqual(subscription.status, "Active")
-		self.assertEqual(len(subscription.invoices), 0)
+		self.assertEqual(len(subscription.invoices), 1)
 
 		subscription.process()
-		self.assertEqual(subscription.status, "Active")
-		self.assertEqual(len(subscription.invoices), 0)
+		self.assertEqual(subscription.status, "Unpaid")
+		self.assertEqual(len(subscription.invoices), 1)
 
 		subscription.process()
-		self.assertEqual(subscription.status, "Active")
-		self.assertEqual(len(subscription.invoices), 0)
+		self.assertEqual(subscription.status, "Unpaid")
+		self.assertEqual(len(subscription.invoices), 1)
 
 		settings.cancel_after_grace = default_grace_period_action
 		settings.save()
-		subscription.delete()
 
 	def test_subscription_unpaid_back_to_active(self):
 		settings = frappe.get_single("Subscription Settings")
@@ -503,8 +499,11 @@
 		subscription.party = "_Test Customer"
 		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 		subscription.start_date = "2018-01-01"
+		subscription.generate_invoice_at_period_start = True
 		subscription.insert()
 
+		frappe.flags.current_date = subscription.current_invoice_start
+
 		subscription.process()  # generate first invoice
 		# This should change status to Unpaid since grace period is 0
 		self.assertEqual(subscription.status, "Unpaid")
@@ -517,12 +516,12 @@
 		self.assertEqual(subscription.status, "Active")
 
 		# A new invoice is generated
+		frappe.flags.current_date = subscription.current_invoice_start
 		subscription.process()
 		self.assertEqual(subscription.status, "Unpaid")
 
 		settings.cancel_after_grace = default_grace_period_action
 		settings.save()
-		subscription.delete()
 
 	def test_restart_active_subscription(self):
 		subscription = frappe.new_doc("Subscription")
@@ -533,8 +532,6 @@
 
 		self.assertRaises(frappe.ValidationError, subscription.restart_subscription)
 
-		subscription.delete()
-
 	def test_subscription_invoice_discount_percentage(self):
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Customer"
@@ -549,8 +546,6 @@
 		self.assertEqual(invoice.additional_discount_percentage, 10)
 		self.assertEqual(invoice.apply_discount_on, "Grand Total")
 
-		subscription.delete()
-
 	def test_subscription_invoice_discount_amount(self):
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Customer"
@@ -565,8 +560,6 @@
 		self.assertEqual(invoice.discount_amount, 11)
 		self.assertEqual(invoice.apply_discount_on, "Grand Total")
 
-		subscription.delete()
-
 	def test_prepaid_subscriptions(self):
 		# Create a non pre-billed subscription, processing should not create
 		# invoices.
@@ -614,8 +607,6 @@
 		settings.prorate = to_prorate
 		settings.save()
 
-		subscription.delete()
-
 	def test_subscription_with_follow_calendar_months(self):
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Supplier"
@@ -623,14 +614,14 @@
 		subscription.generate_invoice_at_period_start = 1
 		subscription.follow_calendar_months = 1
 
-		# select subscription start date as '2018-01-15'
+		# select subscription start date as "2018-01-15"
 		subscription.start_date = "2018-01-15"
 		subscription.end_date = "2018-07-15"
 		subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
 		subscription.save()
 
-		# even though subscription starts at '2018-01-15' and Billing interval is Month and count 3
-		# First invoice will end at '2018-03-31' instead of '2018-04-14'
+		# even though subscription starts at "2018-01-15" and Billing interval is Month and count 3
+		# First invoice will end at "2018-03-31" instead of "2018-04-14"
 		self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31")
 
 	def test_subscription_generate_invoice_past_due(self):
@@ -639,11 +630,12 @@
 		subscription.party = "_Test Supplier"
 		subscription.generate_invoice_at_period_start = 1
 		subscription.generate_new_invoices_past_due_date = 1
-		# select subscription start date as '2018-01-15'
+		# select subscription start date as "2018-01-15"
 		subscription.start_date = "2018-01-01"
 		subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
 		subscription.save()
 
+		frappe.flags.current_date = "2018-01-01"
 		# Process subscription and create first invoice
 		# Subscription status will be unpaid since due date has already passed
 		subscription.process()
@@ -652,8 +644,8 @@
 
 		# Now the Subscription is unpaid
 		# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in
-		# subscription
-
+		# subscription and the interval between the subscriptions is 3 months
+		frappe.flags.current_date = "2018-04-01"
 		subscription.process()
 		self.assertEqual(len(subscription.invoices), 2)
 
@@ -662,7 +654,7 @@
 		subscription.party_type = "Supplier"
 		subscription.party = "_Test Supplier"
 		subscription.generate_invoice_at_period_start = 1
-		# select subscription start date as '2018-01-15'
+		# select subscription start date as "2018-01-15"
 		subscription.start_date = "2018-01-01"
 		subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
 		subscription.save()
@@ -682,7 +674,7 @@
 		subscription.party = "_Test Subscription Customer"
 		subscription.generate_invoice_at_period_start = 1
 		subscription.company = "_Test Company"
-		# select subscription start date as '2018-01-15'
+		# select subscription start date as "2018-01-15"
 		subscription.start_date = "2018-01-01"
 		subscription.append("plans", {"plan": "_Test Plan Multicurrency", "qty": 1})
 		subscription.save()
@@ -692,5 +684,47 @@
 		self.assertEqual(subscription.status, "Unpaid")
 
 		# Check the currency of the created invoice
-		currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].invoice, "currency")
+		currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency")
 		self.assertEqual(currency, "USD")
+
+	def test_subscription_recovery(self):
+		"""Test if Subscription recovers when start/end date run out of sync with created invoices."""
+		subscription = frappe.new_doc("Subscription")
+		subscription.party_type = "Customer"
+		subscription.party = "_Test Subscription Customer"
+		subscription.company = "_Test Company"
+		subscription.start_date = "2021-12-01"
+		subscription.generate_new_invoices_past_due_date = 1
+		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+		subscription.submit_invoice = 0
+		subscription.save()
+
+		# create invoices for the first two moths
+		frappe.flags.current_date = "2021-12-31"
+		subscription.process()
+
+		frappe.flags.current_date = "2022-01-31"
+		subscription.process()
+
+		self.assertEqual(len(subscription.invoices), 2)
+		self.assertEqual(
+			getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
+			getdate("2021-12-01"),
+		)
+		self.assertEqual(
+			getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
+			getdate("2022-01-01"),
+		)
+
+		# recreate most recent invoice
+		subscription.process()
+
+		self.assertEqual(len(subscription.invoices), 2)
+		self.assertEqual(
+			getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
+			getdate("2021-12-01"),
+		)
+		self.assertEqual(
+			getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
+			getdate("2022-01-01"),
+		)
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 58792d1..e66a886 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -476,7 +476,12 @@
 	threshold = tax_details.get("threshold", 0)
 	cumulative_threshold = tax_details.get("cumulative_threshold", 0)
 
-	if (threshold and inv.tax_withholding_net_total >= threshold) or (
+	if inv.doctype != "Payment Entry":
+		tax_withholding_net_total = inv.base_tax_withholding_net_total
+	else:
+		tax_withholding_net_total = inv.tax_withholding_net_total
+
+	if (threshold and tax_withholding_net_total >= threshold) or (
 		cumulative_threshold and supp_credit_amt >= cumulative_threshold
 	):
 		if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index 4580b13..80220e4 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -316,6 +316,42 @@
 		for d in reversed(orders):
 			d.cancel()
 
+	def test_tds_deduction_for_po_via_payment_entry(self):
+		from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+
+		frappe.db.set_value(
+			"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
+		)
+		order = create_purchase_order(supplier="Test TDS Supplier8", rate=40000, do_not_save=True)
+
+		# Add some tax on the order
+		order.append(
+			"taxes",
+			{
+				"category": "Total",
+				"charge_type": "Actual",
+				"account_head": "_Test Account VAT - _TC",
+				"cost_center": "Main - _TC",
+				"tax_amount": 8000,
+				"description": "Test",
+				"add_deduct_tax": "Add",
+			},
+		)
+
+		order.save()
+
+		order.apply_tds = 1
+		order.tax_withholding_category = "Cumulative Threshold TDS"
+		order.submit()
+
+		self.assertEqual(order.taxes[0].tax_amount, 4000)
+
+		payment = get_payment_entry(order.doctype, order.name)
+		payment.apply_tax_withholding_amount = 1
+		payment.tax_withholding_category = "Cumulative Threshold TDS"
+		payment.submit()
+		self.assertEqual(payment.taxes[0].tax_amount, 4000)
+
 	def test_multi_category_single_supplier(self):
 		frappe.db.set_value(
 			"Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category"
@@ -573,6 +609,7 @@
 		"Test TDS Supplier5",
 		"Test TDS Supplier6",
 		"Test TDS Supplier7",
+		"Test TDS Supplier8",
 	]:
 		if frappe.db.exists("Supplier", name):
 			continue
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index db366cf..0d67752 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -14,6 +14,7 @@
 from frappe.contacts.doctype.contact.contact import get_contact_details
 from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
 from frappe.model.utils import get_fetch_values
+from frappe.query_builder.functions import Date, Sum
 from frappe.utils import (
 	add_days,
 	add_months,
@@ -921,32 +922,35 @@
 
 
 def get_partywise_advanced_payment_amount(
-	party_type, posting_date=None, future_payment=0, company=None, party=None
+	party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
 ):
-	cond = "1=1"
+	gle = frappe.qb.DocType("GL Entry")
+	query = (
+		frappe.qb.from_(gle)
+		.select(gle.party)
+		.where(
+			(gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
+		)
+		.groupby(gle.party)
+	)
+	if account_type == "Receivable":
+		query = query.select(Sum(gle.credit).as_("amount"))
+	else:
+		query = query.select(Sum(gle.debit).as_("amount"))
+
 	if posting_date:
 		if future_payment:
-			cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date)
+			query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
 		else:
-			cond = "posting_date <= '{0}'".format(posting_date)
+			query = query.where(gle.posting_date <= posting_date)
 
 	if company:
-		cond += "and company = {0}".format(frappe.db.escape(company))
+		query = query.where(gle.company == company)
 
 	if party:
-		cond += "and party = {0}".format(frappe.db.escape(party))
+		query = query.where(gle.party == party)
 
-	data = frappe.db.sql(
-		""" SELECT party, sum({0}) as amount
-		FROM `tabGL Entry`
-		WHERE
-			party_type = %s and against_voucher is null
-			and is_cancelled = 0
-			and {1} GROUP BY party""".format(
-			("credit") if party_type == "Customer" else "debit", cond
-		),
-		party_type,
-	)
+	data = query.run(as_dict=True)
 	if data:
 		return frappe._dict(data)
 
diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.py b/erpnext/accounts/report/accounts_payable/accounts_payable.py
index 7b19994..8279afb 100644
--- a/erpnext/accounts/report/accounts_payable/accounts_payable.py
+++ b/erpnext/accounts/report/accounts_payable/accounts_payable.py
@@ -7,7 +7,7 @@
 
 def execute(filters=None):
 	args = {
-		"party_type": "Supplier",
+		"account_type": "Payable",
 		"naming_by": ["Buying Settings", "supp_master_name"],
 	}
 	return ReceivablePayableReport(filters).run(args)
diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py
index 65fe1de..834c83c 100644
--- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py
+++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py
@@ -9,7 +9,7 @@
 
 def execute(filters=None):
 	args = {
-		"party_type": "Supplier",
+		"account_type": "Payable",
 		"naming_by": ["Buying Settings", "supp_master_name"],
 	}
 	return AccountsReceivableSummary(filters).run(args)
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 30f7fb3..11bbb6f 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -7,7 +7,7 @@
 import frappe
 from frappe import _, qb, scrub
 from frappe.query_builder import Criterion
-from frappe.query_builder.functions import Date
+from frappe.query_builder.functions import Date, Sum
 from frappe.utils import cint, cstr, flt, getdate, nowdate
 
 from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -34,7 +34,7 @@
 
 def execute(filters=None):
 	args = {
-		"party_type": "Customer",
+		"account_type": "Receivable",
 		"naming_by": ["Selling Settings", "cust_master_name"],
 	}
 	return ReceivablePayableReport(filters).run(args)
@@ -70,8 +70,11 @@
 			"Company", self.filters.get("company"), "default_currency"
 		)
 		self.currency_precision = get_currency_precision() or 2
-		self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
-		self.party_type = self.filters.party_type
+		self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit"
+		self.account_type = self.filters.account_type
+		self.party_type = frappe.db.get_all(
+			"Party Type", {"account_type": self.account_type}, pluck="name"
+		)
 		self.party_details = {}
 		self.invoices = set()
 		self.skip_total_row = 0
@@ -197,6 +200,7 @@
 			# no invoice, this is an invoice / stand-alone payment / credit note
 			row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
 
+		row.party_type = ple.party_type
 		return row
 
 	def update_voucher_balance(self, ple):
@@ -207,8 +211,9 @@
 			return
 
 		# amount in "Party Currency", if its supplied. If not, amount in company currency
-		if self.filters.get(scrub(self.party_type)):
-			amount = ple.amount_in_account_currency
+		for party_type in self.party_type:
+			if self.filters.get(scrub(party_type)):
+				amount = ple.amount_in_account_currency
 		else:
 			amount = ple.amount
 		amount_in_account_currency = ple.amount_in_account_currency
@@ -362,7 +367,7 @@
 
 	def get_invoice_details(self):
 		self.invoice_details = frappe._dict()
-		if self.party_type == "Customer":
+		if self.account_type == "Receivable":
 			si_list = frappe.db.sql(
 				"""
 				select name, due_date, po_no
@@ -390,7 +395,7 @@
 						d.sales_person
 					)
 
-		if self.party_type == "Supplier":
+		if self.account_type == "Payable":
 			for pi in frappe.db.sql(
 				"""
 				select name, due_date, bill_no, bill_date
@@ -421,8 +426,10 @@
 		# customer / supplier name
 		party_details = self.get_party_details(row.party) or {}
 		row.update(party_details)
-		if self.filters.get(scrub(self.filters.party_type)):
-			row.currency = row.account_currency
+		for party_type in self.party_type:
+			if self.filters.get(scrub(party_type)):
+				row.currency = row.account_currency
+				break
 		else:
 			row.currency = self.company_currency
 
@@ -532,65 +539,67 @@
 						self.future_payments.setdefault((d.invoice_no, d.party), []).append(d)
 
 	def get_future_payments_from_payment_entry(self):
-		return frappe.db.sql(
-			"""
-			select
-				ref.reference_name as invoice_no,
-				payment_entry.party,
-				payment_entry.party_type,
-				payment_entry.posting_date as future_date,
-				ref.allocated_amount as future_amount,
-				payment_entry.reference_no as future_ref
-			from
-				`tabPayment Entry` as payment_entry inner join `tabPayment Entry Reference` as ref
-			on
-				(ref.parent = payment_entry.name)
-			where
-				payment_entry.docstatus < 2
-				and payment_entry.posting_date > %s
-				and payment_entry.party_type = %s
-			""",
-			(self.filters.report_date, self.party_type),
-			as_dict=1,
-		)
+		pe = frappe.qb.DocType("Payment Entry")
+		pe_ref = frappe.qb.DocType("Payment Entry Reference")
+		return (
+			frappe.qb.from_(pe)
+			.inner_join(pe_ref)
+			.on(pe_ref.parent == pe.name)
+			.select(
+				(pe_ref.reference_name).as_("invoice_no"),
+				pe.party,
+				pe.party_type,
+				(pe.posting_date).as_("future_date"),
+				(pe_ref.allocated_amount).as_("future_amount"),
+				(pe.reference_no).as_("future_ref"),
+			)
+			.where(
+				(pe.docstatus < 2)
+				& (pe.posting_date > self.filters.report_date)
+				& (pe.party_type.isin(self.party_type))
+			)
+		).run(as_dict=True)
 
 	def get_future_payments_from_journal_entry(self):
-		if self.filters.get("party"):
-			amount_field = (
-				"jea.debit_in_account_currency - jea.credit_in_account_currency"
-				if self.party_type == "Supplier"
-				else "jea.credit_in_account_currency - jea.debit_in_account_currency"
-			)
-		else:
-			amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit"
-
-		return frappe.db.sql(
-			"""
-			select
-				jea.reference_name as invoice_no,
+		je = frappe.qb.DocType("Journal Entry")
+		jea = frappe.qb.DocType("Journal Entry Account")
+		query = (
+			frappe.qb.from_(je)
+			.inner_join(jea)
+			.on(jea.parent == je.name)
+			.select(
+				jea.reference_name.as_("invoice_no"),
 				jea.party,
 				jea.party_type,
-				je.posting_date as future_date,
-				sum('{0}') as future_amount,
-				je.cheque_no as future_ref
-			from
-				`tabJournal Entry` as je inner join `tabJournal Entry Account` as jea
-			on
-				(jea.parent = je.name)
-			where
-				je.docstatus < 2
-				and je.posting_date > %s
-				and jea.party_type = %s
-				and jea.reference_name is not null and jea.reference_name != ''
-			group by je.name, jea.reference_name
-			having future_amount > 0
-			""".format(
-				amount_field
-			),
-			(self.filters.report_date, self.party_type),
-			as_dict=1,
+				je.posting_date.as_("future_date"),
+				je.cheque_no.as_("future_ref"),
+			)
+			.where(
+				(je.docstatus < 2)
+				& (je.posting_date > self.filters.report_date)
+				& (jea.party_type.isin(self.party_type))
+				& (jea.reference_name.isnotnull())
+				& (jea.reference_name != "")
+			)
 		)
 
+		if self.filters.get("party"):
+			if self.account_type == "Payable":
+				query = query.select(
+					Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
+				)
+			else:
+				query = query.select(
+					Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
+				)
+		else:
+			query = query.select(
+				Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount")
+			)
+
+		query = query.having(qb.Field("future_amount") > 0)
+		return query.run(as_dict=True)
+
 	def allocate_future_payments(self, row):
 		# future payments are captured in additional columns
 		# this method allocates pending future payments against a voucher to
@@ -619,13 +628,17 @@
 			row.future_ref = ", ".join(row.future_ref)
 
 	def get_return_entries(self):
-		doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
+		doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
 		filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
-		party_field = scrub(self.filters.party_type)
-		if self.filters.get(party_field):
-			filters.update({party_field: self.filters.get(party_field)})
+		or_filters = {}
+		for party_type in self.party_type:
+			party_field = scrub(party_type)
+			if self.filters.get(party_field):
+				or_filters.update({party_field: self.filters.get(party_field)})
 		self.return_entries = frappe._dict(
-			frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1)
+			frappe.get_all(
+				doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1
+			)
 		)
 
 	def set_ageing(self, row):
@@ -716,6 +729,7 @@
 			)
 			.where(ple.delinked == 0)
 			.where(Criterion.all(self.qb_selection_filter))
+			.where(Criterion.any(self.or_filters))
 		)
 
 		if self.filters.get("group_by_party"):
@@ -746,16 +760,18 @@
 
 	def prepare_conditions(self):
 		self.qb_selection_filter = []
-		party_type_field = scrub(self.party_type)
-		self.qb_selection_filter.append(self.ple.party_type == self.party_type)
+		self.or_filters = []
+		for party_type in self.party_type:
+			party_type_field = scrub(party_type)
+			self.or_filters.append(self.ple.party_type == party_type)
 
-		self.add_common_filters(party_type_field=party_type_field)
+			self.add_common_filters(party_type_field=party_type_field)
 
-		if party_type_field == "customer":
-			self.add_customer_filters()
+			if party_type_field == "customer":
+				self.add_customer_filters()
 
-		elif party_type_field == "supplier":
-			self.add_supplier_filters()
+			elif party_type_field == "supplier":
+				self.add_supplier_filters()
 
 		if self.filters.cost_center:
 			self.get_cost_center_conditions()
@@ -784,11 +800,10 @@
 			self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
 		else:
 			# get GL with "receivable" or "payable" account_type
-			account_type = "Receivable" if self.party_type == "Customer" else "Payable"
 			accounts = [
 				d.name
 				for d in frappe.get_all(
-					"Account", filters={"account_type": account_type, "company": self.filters.company}
+					"Account", filters={"account_type": self.account_type, "company": self.filters.company}
 				)
 			]
 
@@ -878,7 +893,7 @@
 
 	def get_party_details(self, party):
 		if not party in self.party_details:
-			if self.party_type == "Customer":
+			if self.account_type == "Receivable":
 				fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
 
 				if self.filters.get("sales_partner"):
@@ -901,14 +916,20 @@
 		self.columns = []
 		self.add_column("Posting Date", fieldtype="Date")
 		self.add_column(
-			label=_(self.party_type),
+			label="Party Type",
+			fieldname="party_type",
+			fieldtype="Data",
+			width=100,
+		)
+		self.add_column(
+			label="Party",
 			fieldname="party",
-			fieldtype="Link",
-			options=self.party_type,
+			fieldtype="Dynamic Link",
+			options="party_type",
 			width=180,
 		)
 		self.add_column(
-			label="Receivable Account" if self.party_type == "Customer" else "Payable Account",
+			label=self.account_type + " Account",
 			fieldname="party_account",
 			fieldtype="Link",
 			options="Account",
@@ -916,13 +937,19 @@
 		)
 
 		if self.party_naming_by == "Naming Series":
+			if self.account_type == "Payable":
+				label = "Supplier Name"
+				fieldname = "supplier_name"
+			else:
+				label = "Customer Name"
+				fieldname = "customer_name"
 			self.add_column(
-				_("{0} Name").format(self.party_type),
-				fieldname=scrub(self.party_type) + "_name",
+				label=label,
+				fieldname=fieldname,
 				fieldtype="Data",
 			)
 
-		if self.party_type == "Customer":
+		if self.account_type == "Receivable":
 			self.add_column(
 				_("Customer Contact"),
 				fieldname="customer_primary_contact",
@@ -942,7 +969,7 @@
 
 		self.add_column(label="Due Date", fieldtype="Date")
 
-		if self.party_type == "Supplier":
+		if self.account_type == "Payable":
 			self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")
 			self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date")
 
@@ -952,7 +979,7 @@
 
 		self.add_column(_("Invoiced Amount"), fieldname="invoiced")
 		self.add_column(_("Paid Amount"), fieldname="paid")
-		if self.party_type == "Customer":
+		if self.account_type == "Receivable":
 			self.add_column(_("Credit Note"), fieldname="credit_note")
 		else:
 			# note: fieldname is still `credit_note`
@@ -970,7 +997,7 @@
 			self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
 			self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
 
-		if self.filters.party_type == "Customer":
+		if self.filters.account_type == "Receivable":
 			self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data")
 
 			# comma separated list of linked delivery notes
@@ -991,7 +1018,7 @@
 			if self.filters.sales_partner:
 				self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
 
-		if self.filters.party_type == "Supplier":
+		if self.filters.account_type == "Payable":
 			self.add_column(
 				label=_("Supplier Group"),
 				fieldname="supplier_group",
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index 9c01b1a..da4c9da 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -12,7 +12,7 @@
 
 def execute(filters=None):
 	args = {
-		"party_type": "Customer",
+		"account_type": "Receivable",
 		"naming_by": ["Selling Settings", "cust_master_name"],
 	}
 
@@ -21,7 +21,10 @@
 
 class AccountsReceivableSummary(ReceivablePayableReport):
 	def run(self, args):
-		self.party_type = args.get("party_type")
+		self.account_type = args.get("account_type")
+		self.party_type = frappe.db.get_all(
+			"Party Type", {"account_type": self.account_type}, pluck="name"
+		)
 		self.party_naming_by = frappe.db.get_value(
 			args.get("naming_by")[0], None, args.get("naming_by")[1]
 		)
@@ -35,13 +38,19 @@
 
 		self.get_party_total(args)
 
+		party = None
+		for party_type in self.party_type:
+			if self.filters.get(scrub(party_type)):
+				party = self.filters.get(scrub(party_type))
+
 		party_advance_amount = (
 			get_partywise_advanced_payment_amount(
 				self.party_type,
 				self.filters.report_date,
 				self.filters.show_future_payments,
 				self.filters.company,
-				party=self.filters.get(scrub(self.party_type)),
+				party=party,
+				account_type=self.account_type,
 			)
 			or {}
 		)
@@ -57,9 +66,13 @@
 
 			row.party = party
 			if self.party_naming_by == "Naming Series":
-				row.party_name = frappe.get_cached_value(
-					self.party_type, party, scrub(self.party_type) + "_name"
-				)
+				if self.account_type == "Payable":
+					doctype = "Supplier"
+					fieldname = "supplier_name"
+				else:
+					doctype = "Customer"
+					fieldname = "customer_name"
+				row.party_name = frappe.get_cached_value(doctype, party, fieldname)
 
 			row.update(party_dict)
 
@@ -93,6 +106,7 @@
 
 			# set territory, customer_group, sales person etc
 			self.set_party_details(d)
+			self.party_total[d.party].update({"party_type": d.party_type})
 
 	def init_party_total(self, row):
 		self.party_total.setdefault(
@@ -131,17 +145,27 @@
 	def get_columns(self):
 		self.columns = []
 		self.add_column(
-			label=_(self.party_type),
+			label=_("Party Type"),
+			fieldname="party_type",
+			fieldtype="Data",
+			width=100,
+		)
+		self.add_column(
+			label=_("Party"),
 			fieldname="party",
-			fieldtype="Link",
-			options=self.party_type,
+			fieldtype="Dynamic Link",
+			options="party_type",
 			width=180,
 		)
 
 		if self.party_naming_by == "Naming Series":
-			self.add_column(_("{0} Name").format(self.party_type), fieldname="party_name", fieldtype="Data")
+			self.add_column(
+				label=_("Supplier Name") if self.account_type == "Payable" else _("Customer Name"),
+				fieldname="party_name",
+				fieldtype="Data",
+			)
 
-		credit_debit_label = "Credit Note" if self.party_type == "Customer" else "Debit Note"
+		credit_debit_label = "Credit Note" if self.account_type == "Receivable" else "Debit Note"
 
 		self.add_column(_("Advance Amount"), fieldname="advance")
 		self.add_column(_("Invoiced Amount"), fieldname="invoiced")
@@ -159,7 +183,7 @@
 			self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
 			self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
 
-		if self.party_type == "Customer":
+		if self.account_type == "Receivable":
 			self.add_column(
 				label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory"
 			)
diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js
index 4a4ad4d..c65b9e8 100644
--- a/erpnext/accounts/report/balance_sheet/balance_sheet.js
+++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js
@@ -1,22 +1,26 @@
 // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 // License: GNU General Public License v3. See license.txt
 
-frappe.require("assets/erpnext/js/financial_statements.js", function() {
-	frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statements);
+frappe.require("assets/erpnext/js/financial_statements.js", function () {
+	frappe.query_reports["Balance Sheet"] = $.extend(
+		{},
+		erpnext.financial_statements
+	);
 
-	erpnext.utils.add_dimensions('Balance Sheet', 10);
+	erpnext.utils.add_dimensions("Balance Sheet", 10);
 
 	frappe.query_reports["Balance Sheet"]["filters"].push({
-		"fieldname": "accumulated_values",
-		"label": __("Accumulated Values"),
-		"fieldtype": "Check",
-		"default": 1
+		fieldname: "accumulated_values",
+		label: __("Accumulated Values"),
+		fieldtype: "Check",
+		default: 1,
 	});
+	console.log(frappe.query_reports["Balance Sheet"]["filters"]);
 
 	frappe.query_reports["Balance Sheet"]["filters"].push({
-		"fieldname": "include_default_book_entries",
-		"label": __("Include Default Book Entries"),
-		"fieldtype": "Check",
-		"default": 1
+		fieldname: "include_default_book_entries",
+		label: __("Include Default Book Entries"),
+		fieldtype: "Check",
+		default: 1,
 	});
 });
diff --git a/erpnext/accounts/report/balance_sheet/test_balance_sheet.py b/erpnext/accounts/report/balance_sheet/test_balance_sheet.py
new file mode 100644
index 0000000..3cb6efe
--- /dev/null
+++ b/erpnext/accounts/report/balance_sheet/test_balance_sheet.py
@@ -0,0 +1,51 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import today
+
+from erpnext.accounts.report.balance_sheet.balance_sheet import execute
+
+
+class TestBalanceSheet(FrappeTestCase):
+	def test_balance_sheet(self):
+		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,
+			make_sales_invoice,
+		)
+		from erpnext.accounts.utils import get_fiscal_year
+
+		frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
+		frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 6'")
+		frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
+
+		pi = make_purchase_invoice(
+			company="_Test Company 6",
+			warehouse="Finished Goods - _TC6",
+			expense_account="Cost of Goods Sold - _TC6",
+			cost_center="Main - _TC6",
+			qty=10,
+			rate=100,
+		)
+		si = create_sales_invoice(
+			company="_Test Company 6",
+			debit_to="Debtors - _TC6",
+			income_account="Sales - _TC6",
+			cost_center="Main - _TC6",
+			qty=5,
+			rate=110,
+		)
+		filters = frappe._dict(
+			company="_Test Company 6",
+			period_start_date=today(),
+			period_end_date=today(),
+			periodicity="Yearly",
+		)
+		result = execute(filters)[1]
+		for account_dict in result:
+			if account_dict.get("account") == "Current Liabilities - _TC6":
+				self.assertEqual(account_dict.total, 1000)
+			if account_dict.get("account") == "Current Assets - _TC6":
+				self.assertEqual(account_dict.total, 550)
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 0b583a1..080e45a 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -6,6 +6,7 @@
 
 import frappe
 from frappe import _
+from frappe.query_builder import Criterion
 from frappe.utils import flt, getdate
 
 import erpnext
@@ -359,6 +360,7 @@
 			accounts_by_name,
 			accounts,
 			ignore_closing_entries=False,
+			root_type=root_type,
 		)
 
 	calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year)
@@ -603,6 +605,7 @@
 	accounts_by_name,
 	accounts,
 	ignore_closing_entries=False,
+	root_type=None,
 ):
 	"""Returns a dict like { "account": [gl entries], ... }"""
 
@@ -610,7 +613,6 @@
 		"Company", filters.get("company"), ["lft", "rgt"]
 	)
 
-	additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters)
 	companies = frappe.db.sql(
 		""" select name, default_currency from `tabCompany`
 		where lft >= %(company_lft)s and rgt <= %(company_rgt)s""",
@@ -626,27 +628,43 @@
 	)
 
 	for d in companies:
-		gl_entries = frappe.db.sql(
-			"""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
-			gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
-			acc.account_name, acc.account_number
-			from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
-			{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
-			order by gl.account, gl.posting_date""".format(
-				additional_conditions=additional_conditions
-			),
-			{
-				"from_date": from_date,
-				"to_date": to_date,
-				"lft": root_lft,
-				"rgt": root_rgt,
-				"company": d.name,
-				"finance_book": filters.get("finance_book"),
-				"company_fb": frappe.get_cached_value("Company", d.name, "default_finance_book"),
-			},
-			as_dict=True,
+		gle = frappe.qb.DocType("GL Entry")
+		account = frappe.qb.DocType("Account")
+		query = (
+			frappe.qb.from_(gle)
+			.inner_join(account)
+			.on(account.name == gle.account)
+			.select(
+				gle.posting_date,
+				gle.account,
+				gle.debit,
+				gle.credit,
+				gle.is_opening,
+				gle.company,
+				gle.fiscal_year,
+				gle.debit_in_account_currency,
+				gle.credit_in_account_currency,
+				gle.account_currency,
+				account.account_name,
+				account.account_number,
+			)
+			.where(
+				(gle.company == d.name)
+				& (gle.is_cancelled == 0)
+				& (gle.posting_date <= to_date)
+				& (account.lft >= root_lft)
+				& (account.rgt <= root_rgt)
+			)
+			.orderby(gle.account, gle.posting_date)
 		)
 
+		if root_type:
+			query = query.where(account.root_type == root_type)
+		additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters, d)
+		if additional_conditions:
+			query = query.where(Criterion.all(additional_conditions))
+		gl_entries = query.run(as_dict=True)
+
 		if filters and filters.get("presentation_currency") != d.default_currency:
 			currency_info["company"] = d.name
 			currency_info["company_currency"] = d.default_currency
@@ -716,23 +734,25 @@
 		accounts.insert(idx + 1, args)
 
 
-def get_additional_conditions(from_date, ignore_closing_entries, filters):
+def get_additional_conditions(from_date, ignore_closing_entries, filters, d):
+	gle = frappe.qb.DocType("GL Entry")
 	additional_conditions = []
 
 	if ignore_closing_entries:
-		additional_conditions.append("gl.voucher_type != 'Period Closing Voucher'")
+		additional_conditions.append((gle.voucher_type != "Period Closing Voucher"))
 
 	if from_date:
-		additional_conditions.append("gl.posting_date >= %(from_date)s")
+		additional_conditions.append(gle.posting_date >= from_date)
+
+	finance_book = filters.get("finance_book")
+	company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book")
 
 	if filters.get("include_default_book_entries"):
-		additional_conditions.append(
-			"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
-		)
+		additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None])))
 	else:
-		additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
+		additional_conditions.append((gle.finance_book.isin([finance_book, "", None])))
 
-	return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
+	return additional_conditions
 
 
 def add_total_row(out, root_type, balance_must_be, companies, company_currency):
diff --git a/erpnext/accounts/report/financial_ratios/__init__.py b/erpnext/accounts/report/financial_ratios/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/report/financial_ratios/__init__.py
diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.js b/erpnext/accounts/report/financial_ratios/financial_ratios.js
new file mode 100644
index 0000000..643423d
--- /dev/null
+++ b/erpnext/accounts/report/financial_ratios/financial_ratios.js
@@ -0,0 +1,72 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Financial Ratios"] = {
+	filters: [
+		{
+			fieldname: "company",
+			label: __("Company"),
+			fieldtype: "Link",
+			options: "Company",
+			default: frappe.defaults.get_user_default("Company"),
+			reqd: 1,
+		},
+		{
+			fieldname: "from_fiscal_year",
+			label: __("Start Year"),
+			fieldtype: "Link",
+			options: "Fiscal Year",
+			default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
+			reqd: 1,
+		},
+		{
+			fieldname: "to_fiscal_year",
+			label: __("End Year"),
+			fieldtype: "Link",
+			options: "Fiscal Year",
+			default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
+			reqd: 1,
+		},
+		{
+			fieldname: "periodicity",
+			label: __("Periodicity"),
+			fieldtype: "Data",
+			default: "Yearly",
+			reqd: 1,
+			hidden: 1,
+		},
+		{
+			fieldname: "period_start_date",
+			label: __("From Date"),
+			fieldtype: "Date",
+			default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
+			hidden: 1,
+		},
+		{
+			fieldname: "period_end_date",
+			label: __("To Date"),
+			fieldtype: "Date",
+			default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
+			hidden: 1,
+		},
+	],
+	"formatter": function(value, row, column, data, default_formatter) {
+
+		let heading_ratios = ["Liquidity Ratios", "Solvency Ratios","Turnover Ratios"]
+
+		if (heading_ratios.includes(value)) {
+			value = $(`<span>${value}</span>`);
+			let $value = $(value).css("font-weight", "bold");
+			value = $value.wrap("<p></p>").parent().html();
+		}
+
+		if (heading_ratios.includes(row[1].content) && column.fieldtype == "Float") {
+			column.fieldtype = "Data";
+		}
+
+		value = default_formatter(value, row, column, data);
+
+		return value;
+	},
+};
diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.json b/erpnext/accounts/report/financial_ratios/financial_ratios.json
new file mode 100644
index 0000000..1a2e56b
--- /dev/null
+++ b/erpnext/accounts/report/financial_ratios/financial_ratios.json
@@ -0,0 +1,37 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2023-07-13 16:11:11.925096",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2023-07-13 16:11:11.925096",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Financial Ratios",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Account",
+ "report_name": "Financial Ratios",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Accounts User"
+  },
+  {
+   "role": "Auditor"
+  },
+  {
+   "role": "Sales User"
+  },
+  {
+   "role": "Purchase User"
+  },
+  {
+   "role": "Accounts Manager"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.py b/erpnext/accounts/report/financial_ratios/financial_ratios.py
new file mode 100644
index 0000000..57421eb
--- /dev/null
+++ b/erpnext/accounts/report/financial_ratios/financial_ratios.py
@@ -0,0 +1,296 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.utils import add_days, flt
+
+from erpnext.accounts.report.financial_statements import get_data, get_period_list
+from erpnext.accounts.utils import get_balance_on, get_fiscal_year
+
+
+def execute(filters=None):
+	filters["filter_based_on"] = "Fiscal Year"
+	columns, data = [], []
+
+	setup_filters(filters)
+
+	period_list = get_period_list(
+		filters.from_fiscal_year,
+		filters.to_fiscal_year,
+		filters.period_start_date,
+		filters.period_end_date,
+		filters.filter_based_on,
+		filters.periodicity,
+		company=filters.company,
+	)
+
+	columns, years = get_columns(period_list)
+	data = get_ratios_data(filters, period_list, years)
+
+	return columns, data
+
+
+def setup_filters(filters):
+	if not filters.get("period_start_date"):
+		period_start_date = get_fiscal_year(fiscal_year=filters.from_fiscal_year)[1]
+		filters["period_start_date"] = period_start_date
+
+	if not filters.get("period_end_date"):
+		period_end_date = get_fiscal_year(fiscal_year=filters.to_fiscal_year)[2]
+		filters["period_end_date"] = period_end_date
+
+
+def get_columns(period_list):
+	years = []
+	columns = [
+		{
+			"label": _("Ratios"),
+			"fieldname": "ratio",
+			"fieldtype": "Data",
+			"width": 200,
+		},
+	]
+
+	for period in period_list:
+		columns.append(
+			{
+				"fieldname": period.key,
+				"label": period.label,
+				"fieldtype": "Float",
+				"width": 150,
+			}
+		)
+		years.append(period.key)
+
+	return columns, years
+
+
+def get_ratios_data(filters, period_list, years):
+
+	data = []
+	assets, liabilities, income, expense = get_gl_data(filters, period_list, years)
+
+	current_asset, total_asset = {}, {}
+	current_liability, total_liability = {}, {}
+	net_sales, total_income = {}, {}
+	cogs, total_expense = {}, {}
+	quick_asset = {}
+	direct_expense = {}
+
+	for year in years:
+		total_quick_asset = 0
+		total_net_sales = 0
+		total_cogs = 0
+
+		for d in [
+			[
+				current_asset,
+				total_asset,
+				"Current Asset",
+				year,
+				assets,
+				"Asset",
+				quick_asset,
+				total_quick_asset,
+			],
+			[
+				current_liability,
+				total_liability,
+				"Current Liability",
+				year,
+				liabilities,
+				"Liability",
+				{},
+				0,
+			],
+			[cogs, total_expense, "Cost of Goods Sold", year, expense, "Expense", {}, total_cogs],
+			[direct_expense, direct_expense, "Direct Expense", year, expense, "Expense", {}, 0],
+			[net_sales, total_income, "Direct Income", year, income, "Income", {}, total_net_sales],
+		]:
+			update_balances(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7])
+	add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset)
+	add_solvency_ratios(
+		data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
+	)
+	add_turnover_ratios(
+		data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense
+	)
+
+	return data
+
+
+def get_gl_data(filters, period_list, years):
+	data = {}
+
+	for d in [
+		["Asset", "Debit"],
+		["Liability", "Credit"],
+		["Income", "Credit"],
+		["Expense", "Debit"],
+	]:
+		data[frappe.scrub(d[0])] = get_data(
+			filters.company,
+			d[0],
+			d[1],
+			period_list,
+			only_current_fiscal_year=False,
+			filters=filters,
+		)
+
+	assets, liabilities, income, expense = (
+		data.get("asset"),
+		data.get("liability"),
+		data.get("income"),
+		data.get("expense"),
+	)
+
+	return assets, liabilities, income, expense
+
+
+def add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset):
+	precision = frappe.db.get_single_value("System Settings", "float_precision")
+	data.append({"ratio": "Liquidity Ratios"})
+
+	ratio_data = [["Current Ratio", current_asset], ["Quick Ratio", quick_asset]]
+
+	for d in ratio_data:
+		row = {
+			"ratio": d[0],
+		}
+		for year in years:
+			row[year] = calculate_ratio(d[1].get(year, 0), current_liability.get(year, 0), precision)
+
+		data.append(row)
+
+
+def add_solvency_ratios(
+	data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
+):
+	precision = frappe.db.get_single_value("System Settings", "float_precision")
+	data.append({"ratio": "Solvency Ratios"})
+
+	debt_equity_ratio = {"ratio": "Debt Equity Ratio"}
+	gross_profit_ratio = {"ratio": "Gross Profit Ratio"}
+	net_profit_ratio = {"ratio": "Net Profit Ratio"}
+	return_on_asset_ratio = {"ratio": "Return on Asset Ratio"}
+	return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
+
+	for year in years:
+		profit_after_tax = total_income[year] + total_expense[year]
+		share_holder_fund = total_asset[year] - total_liability[year]
+
+		debt_equity_ratio[year] = calculate_ratio(
+			total_liability.get(year), share_holder_fund, precision
+		)
+		return_on_equity_ratio[year] = calculate_ratio(profit_after_tax, share_holder_fund, precision)
+
+		net_profit_ratio[year] = calculate_ratio(profit_after_tax, net_sales.get(year), precision)
+		gross_profit_ratio[year] = calculate_ratio(
+			net_sales.get(year, 0) - cogs.get(year, 0), net_sales.get(year), precision
+		)
+		return_on_asset_ratio[year] = calculate_ratio(profit_after_tax, total_asset.get(year), precision)
+
+	data.append(debt_equity_ratio)
+	data.append(gross_profit_ratio)
+	data.append(net_profit_ratio)
+	data.append(return_on_asset_ratio)
+	data.append(return_on_equity_ratio)
+
+
+def add_turnover_ratios(
+	data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense
+):
+	precision = frappe.db.get_single_value("System Settings", "float_precision")
+	data.append({"ratio": "Turnover Ratios"})
+
+	avg_data = {}
+	for d in ["Receivable", "Payable", "Stock"]:
+		avg_data[frappe.scrub(d)] = avg_ratio_balance("Receivable", period_list, precision, filters)
+
+	avg_debtors, avg_creditors, avg_stock = (
+		avg_data.get("receivable"),
+		avg_data.get("payable"),
+		avg_data.get("stock"),
+	)
+
+	ratio_data = [
+		["Fixed Asset Turnover Ratio", net_sales, total_asset],
+		["Debtor Turnover Ratio", net_sales, avg_debtors],
+		["Creditor Turnover Ratio", direct_expense, avg_creditors],
+		["Inventory Turnover Ratio", cogs, avg_stock],
+	]
+	for ratio in ratio_data:
+		row = {
+			"ratio": ratio[0],
+		}
+		for year in years:
+			row[year] = calculate_ratio(ratio[1].get(year, 0), ratio[2].get(year, 0), precision)
+
+		data.append(row)
+
+
+def update_balances(
+	ratio_dict,
+	total_dict,
+	account_type,
+	year,
+	root_type_data,
+	root_type,
+	net_dict=None,
+	total_net=0,
+):
+
+	for entry in root_type_data:
+		if not entry.get("parent_account") and entry.get("is_group"):
+			total_dict[year] = entry[year]
+			if account_type == "Direct Expense":
+				total_dict[year] = entry[year] * -1
+
+		if root_type in ("Asset", "Liability"):
+			if entry.get("account_type") == account_type and entry.get("is_group"):
+				ratio_dict[year] = entry.get(year)
+			if entry.get("account_type") in ["Bank", "Cash", "Receivable"] and not entry.get("is_group"):
+				total_net += entry.get(year)
+				net_dict[year] = total_net
+
+		elif root_type == "Income":
+			if entry.get("account_type") == account_type and entry.get("is_group"):
+				total_net += entry.get(year)
+				ratio_dict[year] = total_net
+		elif root_type == "Expense" and account_type == "Cost of Goods Sold":
+			if entry.get("account_type") == account_type:
+				total_net += entry.get(year)
+				ratio_dict[year] = total_net
+		else:
+			if entry.get("account_type") == account_type and entry.get("is_group"):
+				ratio_dict[year] = entry.get(year)
+
+
+def avg_ratio_balance(account_type, period_list, precision, filters):
+	avg_ratio = {}
+	for period in period_list:
+		opening_date = add_days(period["from_date"], -1)
+		closing_date = period["to_date"]
+
+		closing_balance = get_balance_on(
+			date=closing_date,
+			company=filters.company,
+			account_type=account_type,
+		)
+		opening_balance = get_balance_on(
+			date=opening_date,
+			company=filters.company,
+			account_type=account_type,
+		)
+		avg_ratio[period["key"]] = flt(
+			(flt(closing_balance) + flt(opening_balance)) / 2, precision=precision
+		)
+
+	return avg_ratio
+
+
+def calculate_ratio(value, denominator, precision):
+	if flt(denominator):
+		return flt(flt(value) / denominator, precision)
+	return 0
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index 26bf315..a76dea6 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -188,6 +188,7 @@
 			filters,
 			gl_entries_by_account,
 			ignore_closing_entries=ignore_closing_entries,
+			root_type=root_type,
 		)
 
 	calculate_values(
@@ -417,13 +418,28 @@
 	gl_entries_by_account,
 	ignore_closing_entries=False,
 	ignore_opening_entries=False,
+	root_type=None,
 ):
 	"""Returns a dict like { "account": [gl entries], ... }"""
 	gl_entries = []
 
+	account_filters = {
+		"company": company,
+		"is_group": 0,
+		"lft": (">=", root_lft),
+		"rgt": ("<=", root_rgt),
+	}
+
+	if root_type:
+		account_filters.update(
+			{
+				"root_type": root_type,
+			}
+		)
+
 	accounts_list = frappe.db.get_all(
 		"Account",
-		filters={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)},
+		filters=account_filters,
 		pluck="name",
 	)
 
diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py
diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js
new file mode 100644
index 0000000..7e6b053
--- /dev/null
+++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js
@@ -0,0 +1,52 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+function get_filters() {
+	let filters = [
+		{
+			"fieldname":"company",
+			"label": __("Company"),
+			"fieldtype": "Link",
+			"options": "Company",
+			"default": frappe.defaults.get_user_default("Company"),
+			"reqd": 1
+		},
+		{
+			"fieldname":"period_start_date",
+			"label": __("Start Date"),
+			"fieldtype": "Date",
+			"reqd": 1,
+			"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
+		},
+		{
+			"fieldname":"period_end_date",
+			"label": __("End Date"),
+			"fieldtype": "Date",
+			"reqd": 1,
+			"default": frappe.datetime.get_today()
+		},
+		{
+			"fieldname":"account",
+			"label": __("Account"),
+			"fieldtype": "MultiSelectList",
+			"options": "Account",
+			get_data: function(txt) {
+				return frappe.db.get_link_options('Account', txt, {
+					company: frappe.query_report.get_filter_value("company"),
+					account_type: ['in', ["Receivable", "Payable"]]
+				});
+			}
+		},
+		{
+			"fieldname":"voucher_no",
+			"label": __("Voucher No"),
+			"fieldtype": "Data",
+			"width": 100,
+		},
+	]
+	return filters;
+}
+
+frappe.query_reports["General and Payment Ledger Comparison"] = {
+	"filters": get_filters()
+};
diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json
new file mode 100644
index 0000000..1d0d9d1
--- /dev/null
+++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2023-08-02 17:30:29.494907",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letterhead": null,
+ "modified": "2023-08-02 17:30:29.494907",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "General and Payment Ledger Comparison",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "GL Entry",
+ "report_name": "General and Payment Ledger Comparison",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Accounts User"
+  },
+  {
+   "role": "Accounts Manager"
+  },
+  {
+   "role": "Auditor"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py
new file mode 100644
index 0000000..553c137
--- /dev/null
+++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py
@@ -0,0 +1,221 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _, qb
+from frappe.query_builder import Criterion
+from frappe.query_builder.functions import Sum
+
+
+class General_Payment_Ledger_Comparison(object):
+	"""
+	A Utility report to compare Voucher-wise balance between General and Payment Ledger
+	"""
+
+	def __init__(self, filters=None):
+		self.filters = filters
+		self.gle = []
+		self.ple = []
+
+	def get_accounts(self):
+		receivable_accounts = [
+			x[0]
+			for x in frappe.db.get_all(
+				"Account",
+				filters={"company": self.filters.company, "account_type": "Receivable"},
+				as_list=True,
+			)
+		]
+		payable_accounts = [
+			x[0]
+			for x in frappe.db.get_all(
+				"Account", filters={"company": self.filters.company, "account_type": "Payable"}, as_list=True
+			)
+		]
+
+		self.account_types = frappe._dict(
+			{
+				"receivable": frappe._dict({"accounts": receivable_accounts, "gle": [], "ple": []}),
+				"payable": frappe._dict({"accounts": payable_accounts, "gle": [], "ple": []}),
+			}
+		)
+
+	def generate_filters(self):
+		if self.filters.account:
+			self.account_types.receivable.accounts = []
+			self.account_types.payable.accounts = []
+
+			for acc in frappe.db.get_all(
+				"Account", filters={"name": ["in", self.filters.account]}, fields=["name", "account_type"]
+			):
+				if acc.account_type == "Receivable":
+					self.account_types.receivable.accounts.append(acc.name)
+				else:
+					self.account_types.payable.accounts.append(acc.name)
+
+	def get_gle(self):
+		gle = qb.DocType("GL Entry")
+
+		for acc_type, val in self.account_types.items():
+			if val.accounts:
+
+				filter_criterion = []
+				if self.filters.voucher_no:
+					filter_criterion.append((gle.voucher_no == self.filters.voucher_no))
+
+				if self.filters.period_start_date:
+					filter_criterion.append(gle.posting_date.gte(self.filters.period_start_date))
+
+				if self.filters.period_end_date:
+					filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
+
+				if acc_type == "receivable":
+					outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
+				else:
+					outstanding = (Sum(gle.credit) - Sum(gle.debit)).as_("outstanding")
+
+				self.account_types[acc_type].gle = (
+					qb.from_(gle)
+					.select(
+						gle.company,
+						gle.account,
+						gle.voucher_no,
+						gle.party,
+						outstanding,
+					)
+					.where(
+						(gle.company == self.filters.company)
+						& (gle.is_cancelled == 0)
+						& (gle.account.isin(val.accounts))
+					)
+					.where(Criterion.all(filter_criterion))
+					.groupby(gle.company, gle.account, gle.voucher_no, gle.party)
+					.run()
+				)
+
+	def get_ple(self):
+		ple = qb.DocType("Payment Ledger Entry")
+
+		for acc_type, val in self.account_types.items():
+			if val.accounts:
+
+				filter_criterion = []
+				if self.filters.voucher_no:
+					filter_criterion.append((ple.voucher_no == self.filters.voucher_no))
+
+				if self.filters.period_start_date:
+					filter_criterion.append(ple.posting_date.gte(self.filters.period_start_date))
+
+				if self.filters.period_end_date:
+					filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
+
+				self.account_types[acc_type].ple = (
+					qb.from_(ple)
+					.select(
+						ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding")
+					)
+					.where(
+						(ple.company == self.filters.company)
+						& (ple.delinked == 0)
+						& (ple.account.isin(val.accounts))
+					)
+					.where(Criterion.all(filter_criterion))
+					.groupby(ple.company, ple.account, ple.voucher_no, ple.party)
+					.run()
+				)
+
+	def compare(self):
+		self.gle_balances = set()
+		self.ple_balances = set()
+
+		# consolidate both receivable and payable balances in one set
+		for acc_type, val in self.account_types.items():
+			self.gle_balances = set(val.gle) | self.gle_balances
+			self.ple_balances = set(val.ple) | self.ple_balances
+
+		self.diff1 = self.gle_balances.difference(self.ple_balances)
+		self.diff2 = self.ple_balances.difference(self.gle_balances)
+		self.diff = frappe._dict({})
+
+		for x in self.diff1:
+			self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
+
+		for x in self.diff2:
+			self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]}))
+
+	def generate_data(self):
+		self.data = []
+		for key, val in self.diff.items():
+			self.data.append(
+				frappe._dict(
+					{
+						"voucher_no": key[2],
+						"party": key[3],
+						"gl_balance": val.gl_balance,
+						"pl_balance": val.pl_balance,
+					}
+				)
+			)
+
+	def get_columns(self):
+		self.columns = []
+		options = None
+		self.columns.append(
+			dict(
+				label=_("Voucher No"),
+				fieldname="voucher_no",
+				fieldtype="Data",
+				options=options,
+				width="100",
+			)
+		)
+
+		self.columns.append(
+			dict(
+				label=_("Party"),
+				fieldname="party",
+				fieldtype="Data",
+				options=options,
+				width="100",
+			)
+		)
+
+		self.columns.append(
+			dict(
+				label=_("GL Balance"),
+				fieldname="gl_balance",
+				fieldtype="Currency",
+				options="Company:company:default_currency",
+				width="100",
+			)
+		)
+
+		self.columns.append(
+			dict(
+				label=_("Payment Ledger Balance"),
+				fieldname="pl_balance",
+				fieldtype="Currency",
+				options="Company:company:default_currency",
+				width="100",
+			)
+		)
+
+	def run(self):
+		self.get_accounts()
+		self.generate_filters()
+		self.get_gle()
+		self.get_ple()
+		self.compare()
+		self.generate_data()
+		self.get_columns()
+
+		return self.columns, self.data
+
+
+def execute(filters=None):
+	columns, data = [], []
+
+	rpt = General_Payment_Ledger_Comparison(filters)
+	columns, data = rpt.run()
+
+	return columns, data
diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py
new file mode 100644
index 0000000..4b0e99d
--- /dev/null
+++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py
@@ -0,0 +1,100 @@
+import unittest
+
+import frappe
+from frappe import qb
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days
+
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.report.general_and_payment_ledger_comparison.general_and_payment_ledger_comparison import (
+	execute,
+)
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+
+
+class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin):
+	def setUp(self):
+		self.create_company()
+		self.cleanup()
+
+	def tearDown(self):
+		frappe.db.rollback()
+
+	def cleanup(self):
+		doctypes = []
+		doctypes.append(qb.DocType("GL Entry"))
+		doctypes.append(qb.DocType("Payment Ledger Entry"))
+		doctypes.append(qb.DocType("Sales Invoice"))
+
+		for doctype in doctypes:
+			qb.from_(doctype).delete().where(doctype.company == self.company).run()
+
+	def test_01_basic_report_functionality(self):
+		sinv = create_sales_invoice(
+			company=self.company,
+			debit_to=self.debit_to,
+			expense_account=self.expense_account,
+			cost_center=self.cost_center,
+			income_account=self.income_account,
+			warehouse=self.warehouse,
+		)
+
+		# manually edit the payment ledger entry
+		ple = frappe.db.get_all(
+			"Payment Ledger Entry", filters={"voucher_no": sinv.name, "delinked": 0}
+		)[0]
+		frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", sinv.grand_total - 1)
+
+		filters = frappe._dict({"company": self.company})
+		columns, data = execute(filters=filters)
+		self.assertEqual(len(data), 1)
+
+		expected = {
+			"voucher_no": sinv.name,
+			"party": sinv.customer,
+			"gl_balance": sinv.grand_total,
+			"pl_balance": sinv.grand_total - 1,
+		}
+		self.assertEqual(expected, data[0])
+
+		# account filter
+		filters = frappe._dict({"company": self.company, "account": self.debit_to})
+		columns, data = execute(filters=filters)
+		self.assertEqual(len(data), 1)
+		self.assertEqual(expected, data[0])
+
+		filters = frappe._dict({"company": self.company, "account": self.creditors})
+		columns, data = execute(filters=filters)
+		self.assertEqual([], data)
+
+		# voucher_no filter
+		filters = frappe._dict({"company": self.company, "voucher_no": sinv.name})
+		columns, data = execute(filters=filters)
+		self.assertEqual(len(data), 1)
+		self.assertEqual(expected, data[0])
+
+		filters = frappe._dict({"company": self.company, "voucher_no": sinv.name + "-1"})
+		columns, data = execute(filters=filters)
+		self.assertEqual([], data)
+
+		# date range filter
+		filters = frappe._dict(
+			{
+				"company": self.company,
+				"period_start_date": sinv.posting_date,
+				"period_end_date": sinv.posting_date,
+			}
+		)
+		columns, data = execute(filters=filters)
+		self.assertEqual(len(data), 1)
+		self.assertEqual(expected, data[0])
+
+		filters = frappe._dict(
+			{
+				"company": self.company,
+				"period_start_date": add_days(sinv.posting_date, -1),
+				"period_end_date": add_days(sinv.posting_date, -1),
+			}
+		)
+		columns, data = execute(filters=filters)
+		self.assertEqual([], data)
diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
index e794f27..9fe93b9 100644
--- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
+++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
@@ -1,19 +1,18 @@
 // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 // License: GNU General Public License v3. See license.txt
 
-
-frappe.require("assets/erpnext/js/financial_statements.js", function() {
-	frappe.query_reports["Profit and Loss Statement"] = $.extend({},
-		erpnext.financial_statements);
-
-	erpnext.utils.add_dimensions('Profit and Loss Statement', 10);
-
-	frappe.query_reports["Profit and Loss Statement"]["filters"].push(
-		{
-			"fieldname": "include_default_book_entries",
-			"label": __("Include Default Book Entries"),
-			"fieldtype": "Check",
-			"default": 1
-		}
+frappe.require("assets/erpnext/js/financial_statements.js", function () {
+	frappe.query_reports["Profit and Loss Statement"] = $.extend(
+		{},
+		erpnext.financial_statements
 	);
+
+	erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
+
+	frappe.query_reports["Profit and Loss Statement"]["filters"].push({
+		fieldname: "accumulated_values",
+		label: __("Accumulated Values"),
+		fieldtype: "Check",
+		default: 1,
+	});
 });
diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py
index 5ab3611..bd9e9fc 100644
--- a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py
+++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py
@@ -46,6 +46,7 @@
 		.select(
 			gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit")
 		)
+		.where(gle.is_cancelled == 0)
 		.groupby(gle.voucher_no)
 	)
 	query = apply_filters(query, filters, gle)
diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py
index c82164e..70bbf7e 100644
--- a/erpnext/accounts/test/accounts_mixin.py
+++ b/erpnext/accounts/test/accounts_mixin.py
@@ -4,7 +4,7 @@
 
 
 class AccountsTestMixin:
-	def create_customer(self, customer_name, currency=None):
+	def create_customer(self, customer_name="_Test Customer", currency=None):
 		if not frappe.db.exists("Customer", customer_name):
 			customer = frappe.new_doc("Customer")
 			customer.customer_name = customer_name
@@ -17,7 +17,7 @@
 		else:
 			self.customer = customer_name
 
-	def create_supplier(self, supplier_name, currency=None):
+	def create_supplier(self, supplier_name="_Test Supplier", currency=None):
 		if not frappe.db.exists("Supplier", supplier_name):
 			supplier = frappe.new_doc("Supplier")
 			supplier.supplier_name = supplier_name
@@ -31,7 +31,7 @@
 		else:
 			self.supplier = supplier_name
 
-	def create_item(self, item_name, is_stock=0, warehouse=None, company=None):
+	def create_item(self, item_name="_Test Item", is_stock=0, warehouse=None, company=None):
 		item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company)
 		self.item = item.name
 
@@ -62,19 +62,44 @@
 		self.debit_usd = "Debtors USD - " + abbr
 		self.cash = "Cash - " + abbr
 		self.creditors = "Creditors - " + abbr
+		self.retained_earnings = "Retained Earnings - " + abbr
 
-		# create bank account
-		bank_account = "HDFC - " + abbr
-		if frappe.db.exists("Account", bank_account):
-			self.bank = bank_account
-		else:
-			bank_acc = frappe.get_doc(
+		# Deferred revenue, expense and bank accounts
+		other_accounts = [
+			frappe._dict(
 				{
-					"doctype": "Account",
+					"attribute_name": "deferred_revenue",
+					"account_name": "Deferred Revenue",
+					"parent_account": "Current Liabilities - " + abbr,
+				}
+			),
+			frappe._dict(
+				{
+					"attribute_name": "deferred_expense",
+					"account_name": "Deferred Expense",
+					"parent_account": "Current Assets - " + abbr,
+				}
+			),
+			frappe._dict(
+				{
+					"attribute_name": "bank",
 					"account_name": "HDFC",
 					"parent_account": "Bank Accounts - " + abbr,
-					"company": self.company,
 				}
-			)
-			bank_acc.save()
-			self.bank = bank_acc.name
+			),
+		]
+		for acc in other_accounts:
+			acc_name = acc.account_name + " - " + abbr
+			if frappe.db.exists("Account", acc_name):
+				setattr(self, acc.attribute_name, acc_name)
+			else:
+				new_acc = frappe.get_doc(
+					{
+						"doctype": "Account",
+						"account_name": acc.account_name,
+						"parent_account": acc.parent_account,
+						"company": self.company,
+					}
+				)
+				new_acc.save()
+				setattr(self, acc.attribute_name, new_acc.name)
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..c24442e 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -179,6 +179,7 @@
 	in_account_currency=True,
 	cost_center=None,
 	ignore_account_permission=False,
+	account_type=None,
 ):
 	if not account and frappe.form_dict.get("account"):
 		account = frappe.form_dict.get("account")
@@ -254,6 +255,21 @@
 		else:
 			cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
 
+	if account_type:
+		accounts = frappe.db.get_all(
+			"Account",
+			filters={"company": company, "account_type": account_type, "is_group": 0},
+			pluck="name",
+			order_by="lft",
+		)
+
+		cond.append(
+			"""
+			gle.account in (%s)
+		"""
+			% (", ".join([frappe.db.escape(account) for account in accounts]))
+		)
+
 	if party_type and party:
 		cond.append(
 			"""gle.party_type = %s and gle.party = %s """
@@ -263,7 +279,8 @@
 	if company:
 		cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
 
-	if account or (party_type and party):
+	if account or (party_type and party) or account_type:
+
 		if in_account_currency:
 			select_field = "sum(debit_in_account_currency) - sum(credit_in_account_currency)"
 		else:
@@ -276,7 +293,6 @@
 				select_field, " and ".join(cond)
 			)
 		)[0][0]
-
 		# if bal is None, return 0
 		return flt(bal)
 
@@ -459,6 +475,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 +637,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 +659,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 +1857,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/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index a97ea73..0a2f61d 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -207,34 +207,39 @@
 	},
 
 	render_depreciation_schedule_view: function(frm, depr_schedule) {
-		var wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty();
+		let wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty();
 
-		let table = $(`<table class="table table-bordered" style="margin-top:0px;">
-			<thead>
-				<tr>
-					<td align="center">${__("No.")}</td>
-					<td>${__("Schedule Date")}</td>
-					<td align="right">${__("Depreciation Amount")}</td>
-					<td align="right">${__("Accumulated Depreciation Amount")}</td>
-					<td>${__("Journal Entry")}</td>
-				</tr>
-			</thead>
-			<tbody></tbody>
-		</table>`);
+		let data = [];
 
 		depr_schedule.forEach((sch) => {
-			const row = $(`<tr>
-				<td align="center">${sch['idx']}</td>
-				<td><b>${frappe.format(sch['schedule_date'], { fieldtype: 'Date' })}</b></td>
-				<td><b>${frappe.format(sch['depreciation_amount'], { fieldtype: 'Currency' })}</b></td>
-				<td>${frappe.format(sch['accumulated_depreciation_amount'], { fieldtype: 'Currency' })}</td>
-				<td><a href="/app/journal-entry/${sch['journal_entry'] || ''}">${sch['journal_entry'] || ''}</a></td>
-			</tr>`);
-			table.find("tbody").append(row);
+			const row = [
+				sch['idx'],
+				frappe.format(sch['schedule_date'], { fieldtype: 'Date' }),
+				frappe.format(sch['depreciation_amount'], { fieldtype: 'Currency' }),
+				frappe.format(sch['accumulated_depreciation_amount'], { fieldtype: 'Currency' }),
+				sch['journal_entry'] || ''
+			];
+			data.push(row);
 		});
 
-		wrapper.append(table);
+		let datatable = new frappe.DataTable(wrapper.get(0), {
+			columns: [
+				{name: __("No."), editable: false, resizable: false, format: value => value, width: 60},
+				{name: __("Schedule Date"), editable: false, resizable: false, width: 270},
+				{name: __("Depreciation Amount"), editable: false, resizable: false, width: 164},
+				{name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164},
+				{name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 312}
+			],
+			data: data,
+			serialNoColumn: false,
+			checkboxColumn: true,
+			cellHeight: 35
+		});
 
+		datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem'});
+		datatable.style.setStyle(`.dt-cell--col-1`, {'text-align': 'center'});
+		datatable.style.setStyle(`.dt-cell--col-2`, {'font-weight': 600});
+		datatable.style.setStyle(`.dt-cell--col-3`, {'font-weight': 600});
 	},
 
 	setup_chart_and_depr_schedule_view: async function(frm) {
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 698fc78..befb524 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -43,6 +43,7 @@
   "column_break_33",
   "opening_accumulated_depreciation",
   "number_of_depreciations_booked",
+  "is_fully_depreciated",
   "section_break_36",
   "finance_books",
   "section_break_33",
@@ -205,6 +206,7 @@
    "fieldname": "disposal_date",
    "fieldtype": "Date",
    "label": "Disposal Date",
+   "no_copy": 1,
    "read_only": 1
   },
   {
@@ -244,19 +246,17 @@
    "label": "Is Existing Asset"
   },
   {
-   "depends_on": "is_existing_asset",
+   "depends_on": "eval:(doc.is_existing_asset)",
    "fieldname": "opening_accumulated_depreciation",
    "fieldtype": "Currency",
    "label": "Opening Accumulated Depreciation",
-   "no_copy": 1,
    "options": "Company:company:default_currency"
   },
   {
-   "depends_on": "eval:(doc.is_existing_asset && doc.opening_accumulated_depreciation)",
+   "depends_on": "eval:(doc.is_existing_asset)",
    "fieldname": "number_of_depreciations_booked",
    "fieldtype": "Int",
-   "label": "Number of Depreciations Booked",
-   "no_copy": 1
+   "label": "Number of Depreciations Booked"
   },
   {
    "collapsible": 1,
@@ -500,6 +500,13 @@
    "fieldtype": "HTML",
    "hidden": 1,
    "label": "Depreciation Schedule View"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval:(doc.is_existing_asset)",
+   "fieldname": "is_fully_depreciated",
+   "fieldtype": "Check",
+   "label": "Is Fully Depreciated"
   }
  ],
  "idx": 72,
@@ -527,13 +534,18 @@
    "link_fieldname": "asset"
   },
   {
+   "group": "Activity",
+   "link_doctype": "Asset Activity",
+   "link_fieldname": "asset"
+  },
+  {
    "group": "Journal Entry",
    "link_doctype": "Journal Entry",
    "link_fieldname": "reference_name",
    "table_fieldname": "accounts"
   }
  ],
- "modified": "2023-07-26 13:33:36.821534",
+ "modified": "2023-07-28 20:12:44.819616",
  "modified_by": "Administrator",
  "module": "Assets",
  "name": "Asset",
@@ -577,4 +589,4 @@
  "states": [],
  "title_field": "asset_name",
  "track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 5d35808..2060c6c 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -25,6 +25,7 @@
 	get_depreciation_accounts,
 	get_disposal_account_and_cost_center,
 )
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
 from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
 	cancel_asset_depr_schedules,
@@ -59,7 +60,7 @@
 		self.make_asset_movement()
 		if not self.booked_fixed_asset and self.validate_make_gl_entry():
 			self.make_gl_entries()
-		if not self.split_from:
+		if self.calculate_depreciation and not self.split_from:
 			asset_depr_schedules_names = make_draft_asset_depr_schedules_if_not_present(self)
 			convert_draft_asset_depr_schedules_into_active(self)
 			if asset_depr_schedules_names:
@@ -71,6 +72,7 @@
 						"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
 					).format(asset_depr_schedules_links)
 				)
+		add_asset_activity(self.name, _("Asset submitted"))
 
 	def on_cancel(self):
 		self.validate_cancellation()
@@ -81,9 +83,10 @@
 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
 		make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name)
 		self.db_set("booked_fixed_asset", 0)
+		add_asset_activity(self.name, _("Asset cancelled"))
 
 	def after_insert(self):
-		if not self.split_from:
+		if self.calculate_depreciation and not self.split_from:
 			asset_depr_schedules_names = make_draft_asset_depr_schedules(self)
 			asset_depr_schedules_links = get_comma_separated_links(
 				asset_depr_schedules_names, "Asset Depreciation Schedule"
@@ -93,6 +96,16 @@
 					"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
 				).format(asset_depr_schedules_links)
 			)
+		if not frappe.db.exists(
+			{
+				"doctype": "Asset Activity",
+				"asset": self.name,
+			}
+		):
+			add_asset_activity(self.name, _("Asset created"))
+
+	def after_delete(self):
+		add_asset_activity(self.name, _("Asset deleted"))
 
 	def validate_asset_and_reference(self):
 		if self.purchase_invoice or self.purchase_receipt:
@@ -135,17 +148,33 @@
 			frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code))
 
 	def validate_cost_center(self):
-		if not self.cost_center:
-			return
-
-		cost_center_company = frappe.db.get_value("Cost Center", self.cost_center, "company")
-		if cost_center_company != self.company:
-			frappe.throw(
-				_("Selected Cost Center {} doesn't belongs to {}").format(
-					frappe.bold(self.cost_center), frappe.bold(self.company)
-				),
-				title=_("Invalid Cost Center"),
+		if self.cost_center:
+			cost_center_company, cost_center_is_group = frappe.db.get_value(
+				"Cost Center", self.cost_center, ["company", "is_group"]
 			)
+			if cost_center_company != self.company:
+				frappe.throw(
+					_("Cost Center {} doesn't belong to Company {}").format(
+						frappe.bold(self.cost_center), frappe.bold(self.company)
+					),
+					title=_("Invalid Cost Center"),
+				)
+			if cost_center_is_group:
+				frappe.throw(
+					_(
+						"Cost Center {} is a group cost center and group cost centers cannot be used in transactions"
+					).format(frappe.bold(self.cost_center)),
+					title=_("Invalid Cost Center"),
+				)
+
+		else:
+			if not frappe.get_cached_value("Company", self.company, "depreciation_cost_center"):
+				frappe.throw(
+					_(
+						"Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}"
+					).format(frappe.bold(self.company)),
+					title=_("Missing Cost Center"),
+				)
 
 	def validate_in_use_date(self):
 		if not self.available_for_use_date:
@@ -194,8 +223,11 @@
 
 		if not self.calculate_depreciation:
 			return
-		elif not self.finance_books:
-			frappe.throw(_("Enter depreciation details"))
+		else:
+			if not self.finance_books:
+				frappe.throw(_("Enter depreciation details"))
+			if self.is_fully_depreciated:
+				frappe.throw(_("Depreciation cannot be calculated for fully depreciated assets"))
 
 		if self.is_existing_asset:
 			return
@@ -276,7 +308,7 @@
 			depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
 			if flt(self.opening_accumulated_depreciation) > depreciable_amount:
 				frappe.throw(
-					_("Opening Accumulated Depreciation must be less than equal to {0}").format(
+					_("Opening Accumulated Depreciation must be less than or equal to {0}").format(
 						depreciable_amount
 					)
 				)
@@ -412,7 +444,9 @@
 					expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
 					value_after_depreciation = self.finance_books[idx].value_after_depreciation
 
-				if flt(value_after_depreciation) <= expected_value_after_useful_life:
+				if (
+					flt(value_after_depreciation) <= expected_value_after_useful_life or self.is_fully_depreciated
+				):
 					status = "Fully Depreciated"
 				elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
 					status = "Partially Depreciated"
@@ -444,7 +478,9 @@
 
 	@frappe.whitelist()
 	def get_manual_depreciation_entries(self):
-		(_, _, depreciation_expense_account) = get_depreciation_accounts(self)
+		(_, _, depreciation_expense_account) = get_depreciation_accounts(
+			self.asset_category, self.company
+		)
 
 		gle = frappe.qb.DocType("GL Entry")
 
@@ -787,10 +823,10 @@
 def make_journal_entry(asset_name):
 	asset = frappe.get_doc("Asset", asset_name)
 	(
-		fixed_asset_account,
+		_,
 		accumulated_depreciation_account,
 		depreciation_expense_account,
-	) = get_depreciation_accounts(asset)
+	) = get_depreciation_accounts(asset.asset_category, asset.company)
 
 	depreciation_cost_center, depreciation_series = frappe.get_cached_value(
 		"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
@@ -898,6 +934,13 @@
 		},
 	)
 
+	add_asset_activity(
+		asset.name,
+		_("Asset updated after being split into Asset {0}").format(
+			get_link_to_form("Asset", new_asset_name)
+		),
+	)
+
 	for row in asset.get("finance_books"):
 		value_after_depreciation = flt(
 			(row.value_after_depreciation * remaining_qty) / asset.asset_quantity
@@ -965,6 +1008,15 @@
 			(row.expected_value_after_useful_life * split_qty) / asset.asset_quantity
 		)
 
+	new_asset.insert()
+
+	add_asset_activity(
+		new_asset.name,
+		_("Asset created after being split from Asset {0}").format(
+			get_link_to_form("Asset", asset.name)
+		),
+	)
+
 	new_asset.submit()
 	new_asset.set_status()
 
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index e1431ea..e2a4b29 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -4,6 +4,8 @@
 
 import frappe
 from frappe import _
+from frappe.query_builder import Order
+from frappe.query_builder.functions import Max, Min
 from frappe.utils import (
 	add_months,
 	cint,
@@ -21,6 +23,7 @@
 	get_checks_for_pl_and_bs_accounts,
 )
 from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
 	get_asset_depr_schedule_doc,
 	get_asset_depr_schedule_name,
@@ -42,11 +45,48 @@
 	failed_asset_names = []
 	error_log_names = []
 
-	for asset_name in get_depreciable_assets(date):
-		asset_doc = frappe.get_doc("Asset", asset_name)
+	depreciable_asset_depr_schedules_data = get_depreciable_asset_depr_schedules_data(date)
+
+	credit_and_debit_accounts_for_asset_category_and_company = {}
+	depreciation_cost_center_and_depreciation_series_for_company = (
+		get_depreciation_cost_center_and_depreciation_series_for_company()
+	)
+
+	accounting_dimensions = get_checks_for_pl_and_bs_accounts()
+
+	for asset_depr_schedule_data in depreciable_asset_depr_schedules_data:
+		(
+			asset_depr_schedule_name,
+			asset_name,
+			asset_category,
+			asset_company,
+			sch_start_idx,
+			sch_end_idx,
+		) = asset_depr_schedule_data
+
+		if (
+			asset_category,
+			asset_company,
+		) not in credit_and_debit_accounts_for_asset_category_and_company:
+			credit_and_debit_accounts_for_asset_category_and_company.update(
+				{
+					(asset_category, asset_company): get_credit_and_debit_accounts_for_asset_category_and_company(
+						asset_category, asset_company
+					),
+				}
+			)
 
 		try:
-			make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
+			make_depreciation_entry(
+				asset_depr_schedule_name,
+				date,
+				sch_start_idx,
+				sch_end_idx,
+				credit_and_debit_accounts_for_asset_category_and_company[(asset_category, asset_company)],
+				depreciation_cost_center_and_depreciation_series_for_company[asset_company],
+				accounting_dimensions,
+			)
+
 			frappe.db.commit()
 		except Exception as e:
 			frappe.db.rollback()
@@ -61,18 +101,36 @@
 	frappe.db.commit()
 
 
-def get_depreciable_assets(date):
-	return frappe.db.sql_list(
-		"""select distinct a.name
-		from tabAsset a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
-		where a.name = ads.asset and ads.name = ds.parent and a.docstatus=1 and ads.docstatus=1
-			and a.status in ('Submitted', 'Partially Depreciated')
-			and a.calculate_depreciation = 1
-			and ds.schedule_date<=%s
-			and ifnull(ds.journal_entry, '')=''""",
-		date,
+def get_depreciable_asset_depr_schedules_data(date):
+	a = frappe.qb.DocType("Asset")
+	ads = frappe.qb.DocType("Asset Depreciation Schedule")
+	ds = frappe.qb.DocType("Depreciation Schedule")
+
+	res = (
+		frappe.qb.from_(ads)
+		.join(a)
+		.on(ads.asset == a.name)
+		.join(ds)
+		.on(ads.name == ds.parent)
+		.select(ads.name, a.name, a.asset_category, a.company, Min(ds.idx) - 1, Max(ds.idx))
+		.where(a.calculate_depreciation == 1)
+		.where(a.docstatus == 1)
+		.where(ads.docstatus == 1)
+		.where(a.status.isin(["Submitted", "Partially Depreciated"]))
+		.where(ds.journal_entry.isnull())
+		.where(ds.schedule_date <= date)
+		.groupby(ads.name)
+		.orderby(a.creation, order=Order.desc)
 	)
 
+	acc_frozen_upto = get_acc_frozen_upto()
+	if acc_frozen_upto:
+		res = res.where(ds.schedule_date > acc_frozen_upto)
+
+	res = res.run()
+
+	return res
+
 
 def make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date=None):
 	for row in asset_doc.get("finance_books"):
@@ -82,8 +140,60 @@
 		make_depreciation_entry(asset_depr_schedule_name, date)
 
 
+def get_acc_frozen_upto():
+	acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
+
+	if not acc_frozen_upto:
+		return
+
+	frozen_accounts_modifier = frappe.db.get_single_value(
+		"Accounts Settings", "frozen_accounts_modifier"
+	)
+
+	if frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator":
+		return getdate(acc_frozen_upto)
+
+	return
+
+
+def get_credit_and_debit_accounts_for_asset_category_and_company(asset_category, company):
+	(
+		_,
+		accumulated_depreciation_account,
+		depreciation_expense_account,
+	) = get_depreciation_accounts(asset_category, company)
+
+	credit_account, debit_account = get_credit_and_debit_accounts(
+		accumulated_depreciation_account, depreciation_expense_account
+	)
+
+	return (credit_account, debit_account)
+
+
+def get_depreciation_cost_center_and_depreciation_series_for_company():
+	company_names = frappe.db.get_all("Company", pluck="name")
+
+	res = {}
+
+	for company_name in company_names:
+		depreciation_cost_center, depreciation_series = frappe.get_cached_value(
+			"Company", company_name, ["depreciation_cost_center", "series_for_depreciation_entry"]
+		)
+		res.update({company_name: (depreciation_cost_center, depreciation_series)})
+
+	return res
+
+
 @frappe.whitelist()
-def make_depreciation_entry(asset_depr_schedule_name, date=None):
+def make_depreciation_entry(
+	asset_depr_schedule_name,
+	date=None,
+	sch_start_idx=None,
+	sch_end_idx=None,
+	credit_and_debit_accounts=None,
+	depreciation_cost_center_and_depreciation_series=None,
+	accounting_dimensions=None,
+):
 	frappe.has_permission("Journal Entry", throw=True)
 
 	if not date:
@@ -91,100 +201,144 @@
 
 	asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name)
 
-	asset_name = asset_depr_schedule_doc.asset
+	asset = frappe.get_doc("Asset", asset_depr_schedule_doc.asset)
 
-	asset = frappe.get_doc("Asset", asset_name)
-	(
-		fixed_asset_account,
-		accumulated_depreciation_account,
-		depreciation_expense_account,
-	) = get_depreciation_accounts(asset)
+	if credit_and_debit_accounts:
+		credit_account, debit_account = credit_and_debit_accounts
+	else:
+		credit_account, debit_account = get_credit_and_debit_accounts_for_asset_category_and_company(
+			asset.asset_category, asset.company
+		)
 
-	depreciation_cost_center, depreciation_series = frappe.get_cached_value(
-		"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
-	)
+	if depreciation_cost_center_and_depreciation_series:
+		depreciation_cost_center, depreciation_series = depreciation_cost_center_and_depreciation_series
+	else:
+		depreciation_cost_center, depreciation_series = frappe.get_cached_value(
+			"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
+		)
 
 	depreciation_cost_center = asset.cost_center or depreciation_cost_center
 
-	accounting_dimensions = get_checks_for_pl_and_bs_accounts()
+	if not accounting_dimensions:
+		accounting_dimensions = get_checks_for_pl_and_bs_accounts()
 
-	for d in asset_depr_schedule_doc.get("depreciation_schedule"):
-		if not d.journal_entry and getdate(d.schedule_date) <= getdate(date):
-			je = frappe.new_doc("Journal Entry")
-			je.voucher_type = "Depreciation Entry"
-			je.naming_series = depreciation_series
-			je.posting_date = d.schedule_date
-			je.company = asset.company
-			je.finance_book = asset_depr_schedule_doc.finance_book
-			je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
+	depreciation_posting_error = None
 
-			credit_account, debit_account = get_credit_and_debit_accounts(
-				accumulated_depreciation_account, depreciation_expense_account
+	for d in asset_depr_schedule_doc.get("depreciation_schedule")[
+		sch_start_idx or 0 : sch_end_idx or len(asset_depr_schedule_doc.get("depreciation_schedule"))
+	]:
+		try:
+			_make_journal_entry_for_depreciation(
+				asset_depr_schedule_doc,
+				asset,
+				date,
+				d,
+				sch_start_idx,
+				sch_end_idx,
+				depreciation_cost_center,
+				depreciation_series,
+				credit_account,
+				debit_account,
+				accounting_dimensions,
 			)
-
-			credit_entry = {
-				"account": credit_account,
-				"credit_in_account_currency": d.depreciation_amount,
-				"reference_type": "Asset",
-				"reference_name": asset.name,
-				"cost_center": depreciation_cost_center,
-			}
-
-			debit_entry = {
-				"account": debit_account,
-				"debit_in_account_currency": d.depreciation_amount,
-				"reference_type": "Asset",
-				"reference_name": asset.name,
-				"cost_center": depreciation_cost_center,
-			}
-
-			for dimension in accounting_dimensions:
-				if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
-					credit_entry.update(
-						{
-							dimension["fieldname"]: asset.get(dimension["fieldname"])
-							or dimension.get("default_dimension")
-						}
-					)
-
-				if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
-					debit_entry.update(
-						{
-							dimension["fieldname"]: asset.get(dimension["fieldname"])
-							or dimension.get("default_dimension")
-						}
-					)
-
-			je.append("accounts", credit_entry)
-
-			je.append("accounts", debit_entry)
-
-			je.flags.ignore_permissions = True
-			je.flags.planned_depr_entry = True
-			je.save()
-
-			d.db_set("journal_entry", je.name)
-
-			if not je.meta.get_workflow():
-				je.submit()
-				idx = cint(asset_depr_schedule_doc.finance_book_id)
-				row = asset.get("finance_books")[idx - 1]
-				row.value_after_depreciation -= d.depreciation_amount
-				row.db_update()
-
-	asset.db_set("depr_entry_posting_status", "Successful")
+			frappe.db.commit()
+		except Exception as e:
+			frappe.db.rollback()
+			depreciation_posting_error = e
 
 	asset.set_status()
 
-	return asset_depr_schedule_doc
+	if not depreciation_posting_error:
+		asset.db_set("depr_entry_posting_status", "Successful")
+		return asset_depr_schedule_doc
+
+	raise depreciation_posting_error
 
 
-def get_depreciation_accounts(asset):
+def _make_journal_entry_for_depreciation(
+	asset_depr_schedule_doc,
+	asset,
+	date,
+	depr_schedule,
+	sch_start_idx,
+	sch_end_idx,
+	depreciation_cost_center,
+	depreciation_series,
+	credit_account,
+	debit_account,
+	accounting_dimensions,
+):
+	if not (sch_start_idx and sch_end_idx) and not (
+		not depr_schedule.journal_entry and getdate(depr_schedule.schedule_date) <= getdate(date)
+	):
+		return
+
+	je = frappe.new_doc("Journal Entry")
+	je.voucher_type = "Depreciation Entry"
+	je.naming_series = depreciation_series
+	je.posting_date = depr_schedule.schedule_date
+	je.company = asset.company
+	je.finance_book = asset_depr_schedule_doc.finance_book
+	je.remark = "Depreciation Entry against {0} worth {1}".format(
+		asset.name, depr_schedule.depreciation_amount
+	)
+
+	credit_entry = {
+		"account": credit_account,
+		"credit_in_account_currency": depr_schedule.depreciation_amount,
+		"reference_type": "Asset",
+		"reference_name": asset.name,
+		"cost_center": depreciation_cost_center,
+	}
+
+	debit_entry = {
+		"account": debit_account,
+		"debit_in_account_currency": depr_schedule.depreciation_amount,
+		"reference_type": "Asset",
+		"reference_name": asset.name,
+		"cost_center": depreciation_cost_center,
+	}
+
+	for dimension in accounting_dimensions:
+		if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
+			credit_entry.update(
+				{
+					dimension["fieldname"]: asset.get(dimension["fieldname"])
+					or dimension.get("default_dimension")
+				}
+			)
+
+		if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
+			debit_entry.update(
+				{
+					dimension["fieldname"]: asset.get(dimension["fieldname"])
+					or dimension.get("default_dimension")
+				}
+			)
+
+	je.append("accounts", credit_entry)
+	je.append("accounts", debit_entry)
+
+	je.flags.ignore_permissions = True
+	je.flags.planned_depr_entry = True
+	je.save()
+
+	depr_schedule.db_set("journal_entry", je.name)
+
+	if not je.meta.get_workflow():
+		je.submit()
+		idx = cint(asset_depr_schedule_doc.finance_book_id)
+		row = asset.get("finance_books")[idx - 1]
+		row.value_after_depreciation -= depr_schedule.depreciation_amount
+		row.db_update()
+
+
+def get_depreciation_accounts(asset_category, company):
 	fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
 
 	accounts = frappe.db.get_value(
 		"Asset Category Account",
-		filters={"parent": asset.asset_category, "company_name": asset.company},
+		filters={"parent": asset_category, "company_name": company},
 		fieldname=[
 			"fixed_asset_account",
 			"accumulated_depreciation_account",
@@ -200,7 +354,7 @@
 
 	if not accumulated_depreciation_account or not depreciation_expense_account:
 		accounts = frappe.get_cached_value(
-			"Company", asset.company, ["accumulated_depreciation_account", "depreciation_expense_account"]
+			"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
 		)
 
 		if not accumulated_depreciation_account:
@@ -215,7 +369,7 @@
 	):
 		frappe.throw(
 			_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
-				asset.asset_category, asset.company
+				asset_category, company
 			)
 		)
 
@@ -325,6 +479,8 @@
 	frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
 	asset.set_status("Scrapped")
 
+	add_asset_activity(asset_name, _("Asset scrapped"))
+
 	frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name))
 
 
@@ -349,6 +505,8 @@
 
 	asset.set_status()
 
+	add_asset_activity(asset_name, _("Asset restored"))
+
 
 def depreciate_asset(asset_doc, date, notes):
 	asset_doc.flags.ignore_validate_update_after_submit = True
@@ -398,6 +556,15 @@
 
 					reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
 					reverse_journal_entry.posting_date = nowdate()
+
+					for account in reverse_journal_entry.accounts:
+						account.update(
+							{
+								"reference_type": "Asset",
+								"reference_name": asset.name,
+							}
+						)
+
 					frappe.flags.is_reverse_depr_entry = True
 					reverse_journal_entry.submit()
 
@@ -551,8 +718,8 @@
 
 
 def get_asset_details(asset, finance_book=None):
-	fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(
-		asset
+	fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
+		asset.asset_category, asset.company
 	)
 	disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
 	depreciation_cost_center = asset.cost_center or depreciation_cost_center
diff --git a/erpnext/assets/doctype/asset_activity/__init__.py b/erpnext/assets/doctype/asset_activity/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/__init__.py
diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.js b/erpnext/assets/doctype/asset_activity/asset_activity.js
new file mode 100644
index 0000000..38d3434
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/asset_activity.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Asset Activity", {
+// 	refresh(frm) {
+
+// 	},
+// });
diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.json b/erpnext/assets/doctype/asset_activity/asset_activity.json
new file mode 100644
index 0000000..476fb27
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/asset_activity.json
@@ -0,0 +1,109 @@
+{
+ "actions": [],
+ "creation": "2023-07-28 12:41:13.232505",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "asset",
+  "column_break_vkdy",
+  "date",
+  "column_break_kkxv",
+  "user",
+  "section_break_romx",
+  "subject"
+ ],
+ "fields": [
+  {
+   "fieldname": "asset",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Asset",
+   "options": "Asset",
+   "print_width": "165",
+   "read_only": 1,
+   "reqd": 1,
+   "width": "165"
+  },
+  {
+   "fieldname": "column_break_vkdy",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_romx",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "subject",
+   "fieldtype": "Small Text",
+   "in_list_view": 1,
+   "label": "Subject",
+   "print_width": "518",
+   "read_only": 1,
+   "reqd": 1,
+   "width": "518"
+  },
+  {
+   "default": "now",
+   "fieldname": "date",
+   "fieldtype": "Datetime",
+   "in_list_view": 1,
+   "label": "Date",
+   "print_width": "158",
+   "read_only": 1,
+   "reqd": 1,
+   "width": "158"
+  },
+  {
+   "fieldname": "user",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "User",
+   "options": "User",
+   "print_width": "150",
+   "read_only": 1,
+   "reqd": 1,
+   "width": "150"
+  },
+  {
+   "fieldname": "column_break_kkxv",
+   "fieldtype": "Column Break"
+  }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2023-08-01 11:09:52.584482",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Activity",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "email": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1
+  },
+  {
+   "email": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts User",
+   "share": 1
+  },
+  {
+   "email": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Quality Manager",
+   "share": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.py b/erpnext/assets/doctype/asset_activity/asset_activity.py
new file mode 100644
index 0000000..28e1b3e
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/asset_activity.py
@@ -0,0 +1,20 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+
+
+class AssetActivity(Document):
+	pass
+
+
+def add_asset_activity(asset, subject):
+	frappe.get_doc(
+		{
+			"doctype": "Asset Activity",
+			"asset": asset,
+			"subject": subject,
+			"user": frappe.session.user,
+		}
+	).insert(ignore_permissions=True, ignore_links=True)
diff --git a/erpnext/assets/doctype/asset_activity/test_asset_activity.py b/erpnext/assets/doctype/asset_activity/test_asset_activity.py
new file mode 100644
index 0000000..7a21559
--- /dev/null
+++ b/erpnext/assets/doctype/asset_activity/test_asset_activity.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestAssetActivity(FrappeTestCase):
+	pass
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index a883bec..324b739 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -18,6 +18,7 @@
 	reset_depreciation_schedule,
 	reverse_depreciation_entry_made_after_disposal,
 )
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
 from erpnext.controllers.stock_controller import StockController
 from erpnext.setup.doctype.brand.brand import get_brand_defaults
@@ -329,7 +330,7 @@
 				gl_entries = self.get_gl_entries()
 
 			if gl_entries:
-				make_gl_entries(gl_entries, from_repost=from_repost)
+				make_gl_entries(gl_entries, merge_entries=False, from_repost=from_repost)
 		elif self.docstatus == 2:
 			make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
 
@@ -359,9 +360,6 @@
 			gl_entries, target_account, target_against, precision
 		)
 
-		if not self.stock_items and not self.service_items and self.are_all_asset_items_non_depreciable:
-			return []
-
 		self.get_gl_entries_for_target_item(gl_entries, target_against, precision)
 
 		return gl_entries
@@ -519,6 +517,13 @@
 			"fixed_asset_account", item=self.target_item_code, company=asset_doc.company
 		)
 
+		add_asset_activity(
+			asset_doc.name,
+			_("Asset created after Asset Capitalization {0} was submitted").format(
+				get_link_to_form("Asset Capitalization", self.name)
+			),
+		)
+
 		frappe.msgprint(
 			_(
 				"Asset {0} has been created. Please set the depreciation details if any and submit it."
@@ -542,9 +547,30 @@
 
 	def set_consumed_asset_status(self, asset):
 		if self.docstatus == 1:
-			asset.set_status("Capitalized" if self.target_is_fixed_asset else "Decapitalized")
+			if self.target_is_fixed_asset:
+				asset.set_status("Capitalized")
+				add_asset_activity(
+					asset.name,
+					_("Asset capitalized after Asset Capitalization {0} was submitted").format(
+						get_link_to_form("Asset Capitalization", self.name)
+					),
+				)
+			else:
+				asset.set_status("Decapitalized")
+				add_asset_activity(
+					asset.name,
+					_("Asset decapitalized after Asset Capitalization {0} was submitted").format(
+						get_link_to_form("Asset Capitalization", self.name)
+					),
+				)
 		else:
 			asset.set_status()
+			add_asset_activity(
+				asset.name,
+				_("Asset restored after Asset Capitalization {0} was cancelled").format(
+					get_link_to_form("Asset Capitalization", self.name)
+				),
+			)
 
 
 @frappe.whitelist()
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py
index b85f719..620aad8 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.py
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.py
@@ -5,6 +5,9 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
+from frappe.utils import get_link_to_form
+
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 
 
 class AssetMovement(Document):
@@ -128,5 +131,24 @@
 				current_location = latest_movement_entry[0][0]
 				current_employee = latest_movement_entry[0][1]
 
-			frappe.db.set_value("Asset", d.asset, "location", current_location)
-			frappe.db.set_value("Asset", d.asset, "custodian", current_employee)
+			frappe.db.set_value("Asset", d.asset, "location", current_location, update_modified=False)
+			frappe.db.set_value("Asset", d.asset, "custodian", current_employee, update_modified=False)
+
+			if current_location and current_employee:
+				add_asset_activity(
+					d.asset,
+					_("Asset received at Location {0} and issued to Employee {1}").format(
+						get_link_to_form("Location", current_location),
+						get_link_to_form("Employee", current_employee),
+					),
+				)
+			elif current_location:
+				add_asset_activity(
+					d.asset,
+					_("Asset transferred to Location {0}").format(get_link_to_form("Location", current_location)),
+				)
+			elif current_employee:
+				add_asset_activity(
+					d.asset,
+					_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)),
+				)
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index f649e51..7e95cb2 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -8,6 +8,7 @@
 import erpnext
 from erpnext.accounts.general_ledger import make_gl_entries
 from erpnext.assets.doctype.asset.asset import get_asset_account
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
 	get_depr_schedule,
 	make_new_active_asset_depr_schedules_and_cancel_current_ones,
@@ -25,8 +26,14 @@
 		self.calculate_total_repair_cost()
 
 	def update_status(self):
-		if self.repair_status == "Pending":
+		if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order":
 			frappe.db.set_value("Asset", self.asset, "status", "Out of Order")
+			add_asset_activity(
+				self.asset,
+				_("Asset out of order due to Asset Repair {0}").format(
+					get_link_to_form("Asset Repair", self.name)
+				),
+			)
 		else:
 			self.asset_doc.set_status()
 
@@ -68,6 +75,13 @@
 			make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
 			self.asset_doc.save()
 
+			add_asset_activity(
+				self.asset,
+				_("Asset updated after completion of Asset Repair {0}").format(
+					get_link_to_form("Asset Repair", self.name)
+				),
+			)
+
 	def before_cancel(self):
 		self.asset_doc = frappe.get_doc("Asset", self.asset)
 
@@ -95,6 +109,13 @@
 			make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
 			self.asset_doc.save()
 
+			add_asset_activity(
+				self.asset,
+				_("Asset updated after cancellation of Asset Repair {0}").format(
+					get_link_to_form("Asset Repair", self.name)
+				),
+			)
+
 	def after_delete(self):
 		frappe.get_doc("Asset", self.asset).set_status()
 
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 8426ed4..823b6e9 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -12,6 +12,7 @@
 )
 from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
 from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
+from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
 from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
 	get_asset_depr_schedule_doc,
 	get_depreciation_amount,
@@ -27,9 +28,21 @@
 	def on_submit(self):
 		self.make_depreciation_entry()
 		self.reschedule_depreciations(self.new_asset_value)
+		add_asset_activity(
+			self.asset,
+			_("Asset's value adjusted after submission of Asset Value Adjustment {0}").format(
+				get_link_to_form("Asset Value Adjustment", self.name)
+			),
+		)
 
 	def on_cancel(self):
 		self.reschedule_depreciations(self.current_asset_value)
+		add_asset_activity(
+			self.asset,
+			_("Asset's value adjusted after cancellation of Asset Value Adjustment {0}").format(
+				get_link_to_form("Asset Value Adjustment", self.name)
+			),
+		)
 
 	def validate_date(self):
 		asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
@@ -51,10 +64,10 @@
 	def make_depreciation_entry(self):
 		asset = frappe.get_doc("Asset", self.asset)
 		(
-			fixed_asset_account,
+			_,
 			accumulated_depreciation_account,
 			depreciation_expense_account,
-		) = get_depreciation_accounts(asset)
+		) = get_depreciation_accounts(asset.asset_category, asset.company)
 
 		depreciation_cost_center, depreciation_series = frappe.get_cached_value(
 			"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
@@ -65,21 +78,23 @@
 		je.naming_series = depreciation_series
 		je.posting_date = self.date
 		je.company = self.company
-		je.remark = _("Depreciation Entry against {0} worth {1}").format(
-			self.asset, self.difference_amount
-		)
+		je.remark = "Depreciation Entry against {0} worth {1}".format(self.asset, self.difference_amount)
 		je.finance_book = self.finance_book
 
 		credit_entry = {
 			"account": accumulated_depreciation_account,
 			"credit_in_account_currency": self.difference_amount,
 			"cost_center": depreciation_cost_center or self.cost_center,
+			"reference_type": "Asset",
+			"reference_name": self.asset,
 		}
 
 		debit_entry = {
 			"account": depreciation_expense_account,
 			"debit_in_account_currency": self.difference_amount,
 			"cost_center": depreciation_cost_center or self.cost_center,
+			"reference_type": "Asset",
+			"reference_name": self.asset,
 		}
 
 		accounting_dimensions = get_checks_for_pl_and_bs_accounts()
diff --git a/erpnext/assets/report/asset_activity/__init__.py b/erpnext/assets/report/asset_activity/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/assets/report/asset_activity/__init__.py
diff --git a/erpnext/assets/report/asset_activity/asset_activity.json b/erpnext/assets/report/asset_activity/asset_activity.json
new file mode 100644
index 0000000..cc46775
--- /dev/null
+++ b/erpnext/assets/report/asset_activity/asset_activity.json
@@ -0,0 +1,33 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2023-08-01 11:14:46.581234",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "letterhead": null,
+ "modified": "2023-08-01 11:14:46.581234",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Activity",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Asset Activity",
+ "report_name": "Asset Activity",
+ "report_type": "Report Builder",
+ "roles": [
+  {
+   "role": "System Manager"
+  },
+  {
+   "role": "Accounts User"
+  },
+  {
+   "role": "Quality Manager"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json
index d810eff..c6b321e 100644
--- a/erpnext/assets/workspace/assets/assets.json
+++ b/erpnext/assets/workspace/assets/assets.json
@@ -183,6 +183,17 @@
    "link_type": "Report",
    "onboard": 0,
    "type": "Link"
+  },
+  {
+   "dependencies": "Asset Activity",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Asset Activity",
+   "link_count": 0,
+   "link_to": "Asset Activity",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
   }
  ],
  "modified": "2023-05-24 14:47:20.243146",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index a7f0304..7c33056 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -366,7 +366,7 @@
 					},
 					allow_child_item_selection: true,
 					child_fieldname: "items",
-					child_columns: ["item_code", "qty"]
+					child_columns: ["item_code", "qty", "ordered_qty"]
 				})
 			}, __("Get Items From"));
 
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index 0cdb915..31a06cf 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -244,19 +244,21 @@
 			]
 		});
 
-		dialog.fields_dict['supplier'].df.onchange = () => {
-			var supplier = dialog.get_value('supplier');
-			frm.call('get_supplier_email_preview', {supplier: supplier}).then(result => {
+		dialog.fields_dict["supplier"].df.onchange = () => {
+			frm.call("get_supplier_email_preview", {
+				supplier: dialog.get_value("supplier"),
+			}).then(({ message }) => {
 				dialog.fields_dict.email_preview.$wrapper.empty();
-				dialog.fields_dict.email_preview.$wrapper.append(result.message);
+				dialog.fields_dict.email_preview.$wrapper.append(
+					message.message
+				);
+				dialog.set_value("subject", message.subject);
 			});
-
-		}
+		};
 
 		dialog.fields_dict.note.$wrapper.append(`<p class="small text-muted">This is a preview of the email to be sent. A PDF of the document will
 			automatically be attached with the email.</p>`);
 
-		dialog.set_value("subject", frm.doc.subject);
 		dialog.show();
 	}
 })
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
index bd65b0c..fbfc1ac 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
@@ -20,11 +20,11 @@
   "items_section",
   "items",
   "supplier_response_section",
-  "salutation",
-  "subject",
-  "col_break_email_1",
   "email_template",
   "preview",
+  "col_break_email_1",
+  "html_llwp",
+  "send_attached_files",
   "sec_break_email_2",
   "message_for_supplier",
   "terms_section_break",
@@ -237,23 +237,6 @@
    "read_only": 1
   },
   {
-   "fetch_from": "email_template.subject",
-   "fetch_if_empty": 1,
-   "fieldname": "subject",
-   "fieldtype": "Data",
-   "label": "Subject",
-   "print_hide": 1
-  },
-  {
-   "description": "Select a greeting for the receiver. E.g. Mr., Ms., etc.",
-   "fieldname": "salutation",
-   "fieldtype": "Link",
-   "label": "Salutation",
-   "no_copy": 1,
-   "options": "Salutation",
-   "print_hide": 1
-  },
-  {
    "fieldname": "col_break_email_1",
    "fieldtype": "Column Break"
   },
@@ -285,13 +268,28 @@
    "fieldname": "named_place",
    "fieldtype": "Data",
    "label": "Named Place"
+  },
+  {
+   "fieldname": "html_llwp",
+   "fieldtype": "HTML",
+   "options": "<p>In your <b>Email Template</b>, you can use the following special variables:\n</p>\n<ul>\n        <li>\n            <code>{{ update_password_link }}</code>: A link where your supplier can set a new password to log into your portal.\n        </li>\n        <li>\n            <code>{{ portal_link }}</code>: A link to this RFQ in your supplier portal.\n        </li>\n        <li>\n            <code>{{ supplier_name }}</code>: The company name of your supplier.\n        </li>\n        <li>\n            <code>{{ contact.salutation }} {{ contact.last_name }}</code>: The contact person of your supplier.\n        </li><li>\n            <code>{{ user_fullname }}</code>: Your full name.\n        </li>\n    </ul>\n<p></p>\n<p>Apart from these, you can access all values in this RFQ, like <code>{{ message_for_supplier }}</code> or <code>{{ terms }}</code>.</p>",
+   "print_hide": 1,
+   "read_only": 1,
+   "report_hide": 1
+  },
+  {
+   "default": "1",
+   "description": "If enabled, all files attached to this document will be attached to each email",
+   "fieldname": "send_attached_files",
+   "fieldtype": "Check",
+   "label": "Send Attached Files"
   }
  ],
  "icon": "fa fa-shopping-cart",
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2023-01-31 23:22:06.684694",
+ "modified": "2023-08-08 16:30:10.870429",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Request for Quotation",
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 4590f8c..e938577 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -116,7 +116,10 @@
 		route = frappe.db.get_value(
 			"Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"]
 		)
-		return get_url("/app/{0}/".format(route) + self.name)
+		if not route:
+			frappe.throw(_("Please add Request for Quotation to the sidebar in Portal Settings."))
+
+		return get_url(f"{route}/{self.name}")
 
 	def update_supplier_part_no(self, supplier):
 		self.vendor = supplier
@@ -179,37 +182,32 @@
 		if full_name == "Guest":
 			full_name = "Administrator"
 
-		# send document dict and some important data from suppliers row
-		# to render message_for_supplier from any template
 		doc_args = self.as_dict()
-		doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")})
 
-		# Get Contact Full Name
-		supplier_name = None
 		if data.get("contact"):
-			contact_name = frappe.db.get_value(
-				"Contact", data.get("contact"), ["first_name", "middle_name", "last_name"]
-			)
-			supplier_name = (" ").join(x for x in contact_name if x)  # remove any blank values
+			contact = frappe.get_doc("Contact", data.get("contact"))
+			doc_args["contact"] = contact.as_dict()
 
-		args = {
-			"update_password_link": update_password_link,
-			"message": frappe.render_template(self.message_for_supplier, doc_args),
-			"rfq_link": rfq_link,
-			"user_fullname": full_name,
-			"supplier_name": supplier_name or data.get("supplier_name"),
-			"supplier_salutation": self.salutation or "Dear Mx.",
-		}
-
-		subject = self.subject or _("Request for Quotation")
-		template = "templates/emails/request_for_quotation.html"
+		doc_args.update(
+			{
+				"supplier": data.get("supplier"),
+				"supplier_name": data.get("supplier_name"),
+				"update_password_link": f'<a href="{update_password_link}" class="btn btn-default btn-xs" target="_blank">{_("Set Password")}</a>',
+				"portal_link": f'<a href="{rfq_link}" class="btn btn-default btn-sm" target="_blank"> {_("Submit your Quotation")} </a>',
+				"user_fullname": full_name,
+			}
+		)
+		email_template = frappe.get_doc("Email Template", self.email_template)
+		message = frappe.render_template(email_template.response_, doc_args)
+		subject = frappe.render_template(email_template.subject, doc_args)
 		sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
-		message = frappe.get_template(template).render(args)
 
 		if preview:
-			return message
+			return {"message": message, "subject": subject}
 
-		attachments = self.get_attachments()
+		attachments = None
+		if self.send_attached_files:
+			attachments = self.get_attachments()
 
 		self.send_email(data, sender, subject, message, attachments)
 
diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
index d250e6f..42fa1d9 100644
--- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
@@ -2,11 +2,14 @@
 # See license.txt
 
 
+from urllib.parse import urlparse
+
 import frappe
 from frappe.tests.utils import FrappeTestCase
 from frappe.utils import nowdate
 
 from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
+	RequestforQuotation,
 	create_supplier_quotation,
 	get_pdf,
 	make_supplier_quotation_from_rfq,
@@ -125,13 +128,18 @@
 		rfq.status = "Draft"
 		rfq.submit()
 
+	def test_get_link(self):
+		rfq = make_request_for_quotation()
+		parsed_link = urlparse(rfq.get_link())
+		self.assertEqual(parsed_link.path, f"/rfq/{rfq.name}")
+
 	def test_get_pdf(self):
 		rfq = make_request_for_quotation()
 		get_pdf(rfq.name, rfq.get("suppliers")[0].supplier)
 		self.assertEqual(frappe.local.response.type, "pdf")
 
 
-def make_request_for_quotation(**args):
+def make_request_for_quotation(**args) -> "RequestforQuotation":
 	"""
 	:param supplier_data: List containing supplier data
 	"""
diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
index 58da851..6e22acf 100644
--- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
+++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
@@ -339,29 +339,35 @@
 		{
 			"min_grade": 0.0,
 			"prevent_rfqs": 1,
+			"warn_rfqs": 0,
 			"notify_supplier": 0,
 			"max_grade": 30.0,
 			"prevent_pos": 1,
+			"warn_pos": 0,
 			"standing_color": "Red",
 			"notify_employee": 0,
 			"standing_name": "Very Poor",
 		},
 		{
 			"min_grade": 30.0,
-			"prevent_rfqs": 1,
+			"prevent_rfqs": 0,
+			"warn_rfqs": 1,
 			"notify_supplier": 0,
 			"max_grade": 50.0,
 			"prevent_pos": 0,
-			"standing_color": "Red",
+			"warn_pos": 1,
+			"standing_color": "Yellow",
 			"notify_employee": 0,
 			"standing_name": "Poor",
 		},
 		{
 			"min_grade": 50.0,
 			"prevent_rfqs": 0,
+			"warn_rfqs": 0,
 			"notify_supplier": 0,
 			"max_grade": 80.0,
 			"prevent_pos": 0,
+			"warn_pos": 0,
 			"standing_color": "Green",
 			"notify_employee": 0,
 			"standing_name": "Average",
@@ -369,9 +375,11 @@
 		{
 			"min_grade": 80.0,
 			"prevent_rfqs": 0,
+			"warn_rfqs": 0,
 			"notify_supplier": 0,
 			"max_grade": 100.0,
 			"prevent_pos": 0,
+			"warn_pos": 0,
 			"standing_color": "Blue",
 			"notify_employee": 0,
 			"standing_name": "Excellent",
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 7940489..fbf97aa 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
@@ -32,13 +32,19 @@
 	apply_pricing_rule_on_transaction,
 	get_applied_pricing_rules,
 )
+from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
 from erpnext.accounts.party import (
 	get_party_account,
 	get_party_account_currency,
 	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 +974,160 @@
 
 				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_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, self.use_company_roundoff_cost_center
+		)
 
-					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)
-						)
+		precision_loss = self.get("base_net_total") - flt(
+			self.get("net_total") * self.conversion_rate, self.precision("net_total")
+		)
 
-					# 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"
+		credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit"
+		against = self.supplier if self.doctype == "Purchase Invoice" else self.customer
 
-					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,
+		if precision_loss:
+			gl_entries.append(
+				self.get_gl_dict(
+					{
+						"account": round_off_account,
+						"against": against,
+						credit_or_debit: precision_loss,
+						"cost_center": round_off_cost_center
+						if self.use_company_roundoff_cost_center
+						else self.cost_center or round_off_cost_center,
+						"remarks": _("Net total calculation precision loss"),
+					}
+				)
+			)
+
+	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"):
+
+							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"
+
+							reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
+
+							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 +1189,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)
 
@@ -1679,8 +1784,13 @@
 				)
 				self.append("payment_schedule", data)
 
+		allocate_payment_based_on_payment_terms = frappe.db.get_value(
+			"Payment Terms Template", self.payment_terms_template, "allocate_payment_based_on_payment_terms"
+		)
+
 		if not (
 			automatically_fetch_payment_terms
+			and allocate_payment_based_on_payment_terms
 			and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
 		):
 			for d in self.get("payment_schedule"):
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index 58cab14..a4bc4a9 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -233,6 +233,9 @@
 				if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"):
 					frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
 
+				if hasattr(d, "item_code") and hasattr(d, "rate") and d.rate < 0:
+					frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code))
+
 				if d.doctype == args["source_dt"] and d.get(args["join_field"]):
 					args["name"] = d.get(args["join_field"])
 
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/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 57339bf..6633f4f 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -550,7 +550,7 @@
 			if rm_obj.serial_and_batch_bundle:
 				args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
 
-			rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args)
+			rm_obj.rate = get_incoming_rate(args)
 
 	def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
 		key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py
new file mode 100644
index 0000000..0f8e133
--- /dev/null
+++ b/erpnext/controllers/tests/test_accounts_controller.py
@@ -0,0 +1,999 @@
+# 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)
+				advances = si.get_advance_entries()
+				self.assertEqual(len(advances), 1)
+				self.assertEqual(advances[0].reference_name, adv.name)
+				si.append(
+					"advances",
+					{
+						"doctype": "Sales Invoice Advance",
+						"reference_type": advances[0].reference_type,
+						"reference_name": advances[0].reference_name,
+						"reference_row": advances[0].reference_row,
+						"advance_amount": 1,
+						"allocated_amount": 1,
+						"ref_exchange_rate": advances[0].exchange_rate,
+						"remarks": advances[0].remarks,
+					},
+				)
+
+				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
+		)
+		advances = si.get_advance_entries()
+		self.assertEqual(len(advances), 1)
+		self.assertEqual(advances[0].reference_name, adv.name)
+		si.append(
+			"advances",
+			{
+				"doctype": "Sales Invoice Advance",
+				"reference_type": advances[0].reference_type,
+				"reference_name": advances[0].reference_name,
+				"advance_amount": 1,
+				"allocated_amount": 1,
+				"ref_exchange_rate": advances[0].exchange_rate,
+				"remarks": advances[0].remarks,
+			},
+		)
+		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)
+		advances = si.get_advance_entries()
+		self.assertEqual(len(advances), 1)
+		self.assertEqual(advances[0].reference_name, adv.name)
+		si.append(
+			"advances",
+			{
+				"doctype": "Sales Invoice Advance",
+				"reference_type": advances[0].reference_type,
+				"reference_name": advances[0].reference_name,
+				"advance_amount": 1,
+				"allocated_amount": 1,
+				"ref_exchange_rate": advances[0].exchange_rate,
+				"remarks": advances[0].remarks,
+			},
+		)
+		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]:
+			with self.subTest(exc_rate=exc_rate):
+				si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True)
+				advances = si.get_advance_entries()
+				self.assertEqual(len(advances), 1)
+				self.assertEqual(advances[0].reference_name, adv.name)
+				si.append(
+					"advances",
+					{
+						"doctype": "Sales Invoice Advance",
+						"reference_type": advances[0].reference_type,
+						"reference_name": advances[0].reference_name,
+						"reference_row": advances[0].reference_row,
+						"advance_amount": 1,
+						"allocated_amount": 1,
+						"ref_exchange_rate": advances[0].exchange_rate,
+						"remarks": advances[0].remarks,
+					},
+				)
+
+				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)
+		advances = si.get_advance_entries()
+		self.assertEqual(len(advances), 1)
+		self.assertEqual(advances[0].reference_name, adv.name)
+		si.append(
+			"advances",
+			{
+				"doctype": "Sales Invoice Advance",
+				"reference_type": advances[0].reference_type,
+				"reference_name": advances[0].reference_name,
+				"reference_row": advances[0].reference_row,
+				"advance_amount": 1,
+				"allocated_amount": 1,
+				"ref_exchange_rate": advances[0].exchange_rate,
+				"remarks": advances[0].remarks,
+			},
+		)
+
+		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/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index a98886c..105c58d 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -182,7 +182,7 @@
 				"last_name": self.last_name,
 				"salutation": self.salutation,
 				"gender": self.gender,
-				"job_title": self.job_title,
+				"designation": self.job_title,
 				"company_name": self.company_name,
 			}
 		)
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index a9eef94..e3bea25 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -439,7 +439,6 @@
 		"erpnext.controllers.accounts_controller.update_invoice_status",
 		"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year",
 		"erpnext.projects.doctype.task.task.set_tasks_as_overdue",
-		"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
 		"erpnext.stock.doctype.serial_no.serial_no.update_maintenance_status",
 		"erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.refresh_scorecards",
 		"erpnext.setup.doctype.company.company.cache_companies_monthly_sales_history",
@@ -464,6 +463,7 @@
 		"erpnext.setup.doctype.email_digest.email_digest.send",
 		"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
 		"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
+		"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
 	],
 	"monthly_long": [
 		"erpnext.accounts.deferred_revenue.process_deferred_accounting",
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py
index d5b6d37..ac271b7 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation.py
@@ -114,7 +114,7 @@
 
 		if schedule_date in tuple(get_holidays(self.holiday_list)):
 			schedule_date = add_days(schedule_date, 1)
-			self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True)
+			return self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True)
 
 		return schedule_date
 
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 0f4238c..d035ad6 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -320,6 +320,8 @@
 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
+erpnext.patches.v14_0.update_subscription_details
 # 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()
diff --git a/erpnext/patches/v14_0/update_subscription_details.py b/erpnext/patches/v14_0/update_subscription_details.py
new file mode 100644
index 0000000..58ab16d
--- /dev/null
+++ b/erpnext/patches/v14_0/update_subscription_details.py
@@ -0,0 +1,17 @@
+import frappe
+
+
+def execute():
+	subscription_invoices = frappe.get_all(
+		"Subscription Invoice", fields=["document_type", "invoice", "parent"]
+	)
+
+	for subscription_invoice in subscription_invoices:
+		frappe.db.set_value(
+			subscription_invoice.document_type,
+			subscription_invoice.invoice,
+			"subscription",
+			subscription_invoice.parent,
+		)
+
+	frappe.delete_doc_if_exists("DocType", "Subscription Invoice")
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index a067ec0..bcad6eb 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -24,12 +24,14 @@
 				fieldtype: 'Data',
 				reqd: 1
 			},
+			{ fieldtype: "Column Break" },
 			{
 				fieldname: 'company_abbr',
 				label: __('Company Abbreviation'),
 				fieldtype: 'Data',
-				hidden: 1
+				reqd: 1
 			},
+			{ fieldtype: "Section Break" },
 			{
 				fieldname: 'chart_of_accounts', label: __('Chart of Accounts'),
 				options: "", fieldtype: 'Select'
@@ -135,18 +137,20 @@
 				me.charts_modal(slide, chart_template);
 			});
 
-			slide.get_input("company_name").on("change", function () {
+			slide.get_input("company_name").on("input", function () {
 				let parts = slide.get_input("company_name").val().split(" ");
 				let abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
 				slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
 			}).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
 
 			slide.get_input("company_abbr").on("change", function () {
-				if (slide.get_input("company_abbr").val().length > 10) {
+				let abbr = slide.get_input("company_abbr").val();
+				if (abbr.length > 10) {
 					frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
-					slide.get_field("company_abbr").set_value("");
+					abbr = abbr.slice(0, 10);
 				}
-			});
+				slide.get_field("company_abbr").set_value(abbr);
+			}).val(frappe.boot.sysdefaults.company_abbr || "").trigger("change");
 		},
 
 		charts_modal: function(slide, chart_template) {
diff --git a/erpnext/quality_management/doctype/non_conformance/non_conformance.json b/erpnext/quality_management/doctype/non_conformance/non_conformance.json
index 8dfe2d6..e6b8744 100644
--- a/erpnext/quality_management/doctype/non_conformance/non_conformance.json
+++ b/erpnext/quality_management/doctype/non_conformance/non_conformance.json
@@ -62,10 +62,10 @@
    "fieldtype": "Column Break"
   },
   {
-   "fetch_from": "process_owner.full_name",
+   "fetch_from": "procedure.process_owner_full_name",
    "fieldname": "full_name",
    "fieldtype": "Data",
-   "hidden": 1,
+   "read_only": 1,
    "label": "Full Name"
   },
   {
@@ -81,7 +81,7 @@
  ],
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2021-02-26 15:27:47.247814",
+ "modified": "2023-07-31 08:10:47.247814",
  "modified_by": "Administrator",
  "module": "Quality Management",
  "name": "Non Conformance",
diff --git a/erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json b/erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json
index 3a750c2..5ddf0f2 100644
--- a/erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json
+++ b/erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json
@@ -56,6 +56,7 @@
    "fieldtype": "Column Break"
   },
   {
+   "default": "Open",
    "columns": 2,
    "fieldname": "status",
    "fieldtype": "Select",
@@ -67,7 +68,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2020-10-27 16:28:20.908637",
+ "modified": "2023-07-31 09:20:20.908637",
  "modified_by": "Administrator",
  "module": "Quality Management",
  "name": "Quality Review Objective",
@@ -76,4 +77,4 @@
  "sort_field": "modified",
  "sort_order": "DESC",
  "track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py
index cc223e9..6ae04c1 100644
--- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py
+++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py
@@ -34,6 +34,7 @@
 				"supplier": self.supplier,
 				"tax_withholding_category": self.tax_withholding_category,
 				"name": ("!=", self.name),
+				"company": self.company,
 			},
 			["name", "valid_from", "valid_upto"],
 			as_dict=True,
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 624dadb..ca669f6 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -50,7 +50,7 @@
 		super(SalesOrder, self).__init__(*args, **kwargs)
 
 	def onload(self) -> None:
-		if frappe.get_cached_value("Stock Settings", None, "enable_stock_reservation"):
+		if frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
 			if self.has_unreserved_stock():
 				self.set_onload("has_unreserved_stock", True)
 
@@ -733,7 +733,7 @@
 		# qty is for packed items, because packed items don't have stock_qty field
 		qty = source.get("qty")
 		target.project = source_parent.project
-		target.qty = qty - requested_item_qty.get(source.name, 0) - source.delivered_qty
+		target.qty = qty - requested_item_qty.get(source.name, 0) - flt(source.get("delivered_qty"))
 		target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
 
 		args = target.as_dict().copy()
@@ -767,7 +767,7 @@
 				"doctype": "Material Request Item",
 				"field_map": {"name": "sales_order_item", "parent": "sales_order"},
 				"condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code)
-				and (doc.stock_qty - doc.delivered_qty) > requested_item_qty.get(doc.name, 0),
+				and (doc.stock_qty - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0),
 				"postprocess": update_item,
 			},
 		},
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 796e258..c85a4fb 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -549,6 +549,26 @@
 		workflow.is_active = 0
 		workflow.save()
 
+	def test_material_request_for_product_bundle(self):
+		# Create the Material Request from the sales order for the Packing Items
+		# Check whether the material request has the correct packing item or not.
+		if not frappe.db.exists("Item", "_Test Product Bundle Item New 1"):
+			bundle_item = make_item("_Test Product Bundle Item New 1", {"is_stock_item": 0})
+			bundle_item.append(
+				"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
+			)
+			bundle_item.save(ignore_permissions=True)
+
+		make_item("_Packed Item New 2", {"is_stock_item": 1})
+		make_product_bundle("_Test Product Bundle Item New 1", ["_Packed Item New 2"], 2)
+
+		so = make_sales_order(
+			item_code="_Test Product Bundle Item New 1",
+		)
+
+		mr = make_material_request(so.name)
+		self.assertEqual(mr.items[0].item_code, "_Packed Item New 2")
+
 	def test_bin_details_of_packed_item(self):
 		# test Update Items with product bundle
 		if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index f4682c1..fa207ec 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -18,6 +18,7 @@
 		});
 	},
 	setup: function(frm) {
+		frm.__rename_queue = "long";
 		erpnext.company.setup_queries(frm);
 
 		frm.set_query("parent_company", function() {
diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py
index 2ef4e65..526bc2b 100644
--- a/erpnext/setup/doctype/holiday_list/holiday_list.py
+++ b/erpnext/setup/doctype/holiday_list/holiday_list.py
@@ -6,12 +6,9 @@
 from datetime import date
 
 import frappe
-from babel import Locale
 from frappe import _, throw
 from frappe.model.document import Document
 from frappe.utils import formatdate, getdate, today
-from holidays import country_holidays
-from holidays.utils import list_supported_countries
 
 
 class OverlapError(frappe.ValidationError):
@@ -40,6 +37,8 @@
 
 	@frappe.whitelist()
 	def get_supported_countries(self):
+		from holidays.utils import list_supported_countries
+
 		subdivisions_by_country = list_supported_countries()
 		countries = [
 			{"value": country, "label": local_country_name(country)}
@@ -52,6 +51,8 @@
 
 	@frappe.whitelist()
 	def get_local_holidays(self):
+		from holidays import country_holidays
+
 		if not self.country:
 			throw(_("Please select a country"))
 
@@ -169,4 +170,6 @@
 
 def local_country_name(country_code: str) -> str:
 	"""Return the localized country name for the given country code."""
-	return Locale.parse(frappe.local.lang).territories.get(country_code, country_code)
+	from babel import Locale
+
+	return Locale.parse(frappe.local.lang, sep="-").territories.get(country_code, country_code)
diff --git a/erpnext/setup/doctype/holiday_list/test_holiday_list.py b/erpnext/setup/doctype/holiday_list/test_holiday_list.py
index 23b08fd..7eeb27d 100644
--- a/erpnext/setup/doctype/holiday_list/test_holiday_list.py
+++ b/erpnext/setup/doctype/holiday_list/test_holiday_list.py
@@ -8,6 +8,8 @@
 import frappe
 from frappe.utils import getdate
 
+from erpnext.setup.doctype.holiday_list.holiday_list import local_country_name
+
 
 class TestHolidayList(unittest.TestCase):
 	def test_holiday_list(self):
@@ -58,6 +60,16 @@
 		self.assertIn(date(2023, 4, 10), holidays)
 		self.assertNotIn(date(2023, 5, 1), holidays)
 
+	def test_localized_country_names(self):
+		lang = frappe.local.lang
+		frappe.local.lang = "en-gb"
+		self.assertEqual(local_country_name("IN"), "India")
+		self.assertEqual(local_country_name("DE"), "Germany")
+
+		frappe.local.lang = "de"
+		self.assertEqual(local_country_name("DE"), "Deutschland")
+		frappe.local.lang = lang
+
 
 def make_holiday_list(
 	name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None
diff --git a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json
index 4115196..66c5bfb 100644
--- a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json
+++ b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json
@@ -2,7 +2,7 @@
  "action": "Create Entry",
  "action_label": "Create a new Item",
  "creation": "2021-05-17 13:47:18.515052",
- "description": "# Create an Item\n\nItem is a product, of a or service offered by your company, or something you buy as a part of your supplies or raw materials.\n\nItems are integral to everything you do in ERPNext - from billing, purchasing to managing inventory. Everything you buy or sell, whether it is a physical product or a service is an Item. Items can be stock, non-stock, variants, serialized, batched, assets etc.\n",
+ "description": "# Create an Item\n\nItem is a product or a service offered by your company, or something you buy as a part of your supplies or raw materials.\n\nItems are integral to everything you do in ERPNext - from billing, purchasing to managing inventory. Everything you buy or sell, whether it is a physical product or a service is an Item. Items can be stock, non-stock, variants, serialized, batched, assets, etc.\n",
  "docstatus": 0,
  "doctype": "Onboarding Step",
  "form_tour": "Item General",
@@ -20,4 +20,4 @@
  "show_full_form": 0,
  "title": "Create an Item",
  "validate_action": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
index b6b5ff4..e66c233 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
@@ -11,6 +11,7 @@
 		},
 		"internal_links": {
 			"Sales Order": ["items", "against_sales_order"],
+			"Sales Invoice": ["items", "against_sales_invoice"],
 			"Material Request": ["items", "material_request"],
 			"Purchase Order": ["items", "purchase_order"],
 		},
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index ef4155e..aff9587 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -395,16 +395,16 @@
 
 	def validate_warehouse_for_reorder(self):
 		"""Validate Reorder level table for duplicate and conditional mandatory"""
-		warehouse = []
+		warehouse_material_request_type: list[tuple[str, str]] = []
 		for d in self.get("reorder_levels"):
 			if not d.warehouse_group:
 				d.warehouse_group = d.warehouse
-			if d.get("warehouse") and d.get("warehouse") not in warehouse:
-				warehouse += [d.get("warehouse")]
+			if (d.get("warehouse"), d.get("material_request_type")) not in warehouse_material_request_type:
+				warehouse_material_request_type += [(d.get("warehouse"), d.get("material_request_type"))]
 			else:
 				frappe.throw(
-					_("Row {0}: An Reorder entry already exists for this warehouse {1}").format(
-						d.idx, d.warehouse
+					_("Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}.").format(
+						d.idx, d.warehouse, d.material_request_type
 					),
 					DuplicateReorderRows,
 				)
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 00b1b20..1139c4b 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -225,7 +225,8 @@
 					d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
 
 					if mr_qty_allowance:
-						allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
+						allowed_qty = flt((d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty"))
+
 						if d.ordered_qty and d.ordered_qty > allowed_qty:
 							frappe.throw(
 								_(
diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py
index 554055f..e77d53a 100644
--- a/erpnext/stock/doctype/price_list/price_list.py
+++ b/erpnext/stock/doctype/price_list/price_list.py
@@ -45,7 +45,7 @@
 
 		doc_before_save = self.get_doc_before_save()
 		currency_changed = self.currency != doc_before_save.currency
-		affects_cart = self.name == frappe.get_cached_value("E Commerce Settings", None, "price_list")
+		affects_cart = self.name == frappe.db.get_single_value("E Commerce Settings", "price_list")
 
 		if currency_changed and affects_cart:
 			validate_cart_settings()
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 0059a3f..a2cae7f 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -187,7 +187,7 @@
 			return False
 
 		# If line items are more than 100 or record is older than 6 months
-		if len(self.items) > 100 or month_diff(nowdate(), self.posting_date) > 6:
+		if len(self.items) > 50 or month_diff(nowdate(), self.posting_date) > 6:
 			return True
 
 		return False
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 6ea27ed..f009bd4 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -696,7 +696,7 @@
 			)
 
 		if sl_entries:
-			self.make_sl_entries(sl_entries)
+			self.make_sl_entries(sl_entries, allow_negative_stock=True)
 
 	def recalculate_qty_for_serial_and_batch_bundle(self, row):
 		doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
index bdc9d74..176a215 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
@@ -17,7 +17,7 @@
 	if not filters:
 		filters = {}
 
-	sle_count = frappe.db.count("Stock Ledger Entry", {"is_cancelled": 0})
+	sle_count = frappe.db.count("Stock Ledger Entry")
 
 	if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
 		frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))
diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.py b/erpnext/stock/report/item_shortage_report/item_shortage_report.py
index 9fafe91..4bd9a10 100644
--- a/erpnext/stock/report/item_shortage_report/item_shortage_report.py
+++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.py
@@ -40,7 +40,12 @@
 			item.item_name,
 			item.description,
 		)
-		.where((bin.projected_qty < 0) & (wh.name == bin.warehouse) & (bin.item_code == item.name))
+		.where(
+			(item.disabled == 0)
+			& (bin.projected_qty < 0)
+			& (wh.name == bin.warehouse)
+			& (bin.item_code == item.name)
+		)
 		.orderby(bin.projected_qty)
 	)
 
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
index 4663209..887cba5 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -506,6 +506,67 @@
 		self.assertNotEqual(scr.supplied_items[0].rate, prev_cost)
 		self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate)
 
+	def test_subcontracting_receipt_raw_material_rate(self):
+		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+
+		# Step - 1: Set Backflush Based On as "BOM"
+		set_backflush_based_on("BOM")
+
+		# Step - 2: Create FG and RM Items
+		fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
+		rm_item1 = make_item(properties={"is_stock_item": 1}).name
+		rm_item2 = make_item(properties={"is_stock_item": 1}).name
+
+		# Step - 3: Create BOM for FG Item
+		bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
+		for rm_item in bom.items:
+			self.assertEqual(rm_item.rate, 0)
+			self.assertEqual(rm_item.amount, 0)
+		bom = bom.name
+
+		# Step - 4: Create PO and SCO
+		service_items = [
+			{
+				"warehouse": "_Test Warehouse - _TC",
+				"item_code": "Subcontracted Service Item 1",
+				"qty": 100,
+				"rate": 100,
+				"fg_item": fg_item,
+				"fg_item_qty": 100,
+			},
+		]
+		sco = get_subcontracting_order(service_items=service_items)
+		for rm_item in sco.supplied_items:
+			self.assertEqual(rm_item.rate, 0)
+			self.assertEqual(rm_item.amount, 0)
+
+		# Step - 5: Inward Raw Materials
+		rm_items = get_rm_items(sco.supplied_items)
+		for rm_item in rm_items:
+			rm_item["rate"] = 100
+		itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+		# Step - 6: Transfer RM's to Subcontractor
+		se = make_stock_transfer_entry(
+			sco_no=sco.name,
+			rm_items=rm_items,
+			itemwise_details=copy.deepcopy(itemwise_details),
+		)
+		for item in se.items:
+			self.assertEqual(item.qty, 100)
+			self.assertEqual(item.basic_rate, 100)
+			self.assertEqual(item.amount, item.qty * item.basic_rate)
+
+		# Step - 7: Create Subcontracting Receipt
+		scr = make_subcontracting_receipt(sco.name)
+		scr.save()
+		scr.submit()
+		scr.load_from_db()
+		for rm_item in scr.supplied_items:
+			self.assertEqual(rm_item.consumed_qty, 100)
+			self.assertEqual(rm_item.rate, 100)
+			self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
+
 
 def make_return_subcontracting_receipt(**args):
 	args = frappe._dict(args)
diff --git a/erpnext/templates/emails/request_for_quotation.html b/erpnext/templates/emails/request_for_quotation.html
deleted file mode 100644
index 5b073e6..0000000
--- a/erpnext/templates/emails/request_for_quotation.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<h4>{{_("Request for Quotation")}}</h4>
-<p>{{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},</p>
-<p>{{ message }}</p>
-<p>{{_("The Request for Quotation can be accessed by clicking on the following button")}}:</p>
-<br>
-<a
-	href="{{ rfq_link }}"
-	class="btn btn-default btn-sm"
-	target="_blank">
-	{{ _("Submit your Quotation") }}
-</a>
-<br>
-<br>
-{% if update_password_link %}
-<br>
-<p>{{_("Please click on the following button to set your new password")}}:</p>
-<a
-	href="{{ update_password_link }}"
-	class="btn btn-default btn-xs"
-	target="_blank">
-	{{_("Set Password") }}
-</a>
-<br>
-<br>
-{% endif %}
-<p>
-	{{_("Regards")}},<br>
-	{{ user_fullname }}
-</p>
diff --git a/erpnext/translations/af.csv b/erpnext/translations/af.csv
index 35ccbb6..417f1ec 100644
--- a/erpnext/translations/af.csv
+++ b/erpnext/translations/af.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Jy kan nie projektipe &#39;eksterne&#39; uitvee nie,
 You cannot edit root node.,U kan nie wortelknoop wysig nie.,
 You cannot restart a Subscription that is not cancelled.,U kan nie &#39;n intekening herlaai wat nie gekanselleer is nie.,
-You don't have enought Loyalty Points to redeem,U het nie genoeg lojaliteitspunte om te verkoop nie,
+You don't have enough Loyalty Points to redeem,U het nie genoeg lojaliteitspunte om te verkoop nie,
 You have already assessed for the assessment criteria {}.,U het reeds geassesseer vir die assesseringskriteria ().,
 You have already selected items from {0} {1},Jy het reeds items gekies van {0} {1},
 You have been invited to collaborate on the project: {0},U is genooi om saam te werk aan die projek: {0},
diff --git a/erpnext/translations/am.csv b/erpnext/translations/am.csv
index da865b8..b5abbbf 100644
--- a/erpnext/translations/am.csv
+++ b/erpnext/translations/am.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',የፕሮጀክት አይነት «ውጫዊ» ን መሰረዝ አይችሉም.,
 You cannot edit root node.,የስር ሥፍራ ማረም አይችሉም.,
 You cannot restart a Subscription that is not cancelled.,የማይሰረዝ የደንበኝነት ምዝገባን ዳግም ማስጀመር አይችሉም.,
-You don't have enought Loyalty Points to redeem,ለማስመለስ በቂ የታማኝነት ነጥቦች የሉዎትም,
+You don't have enough Loyalty Points to redeem,ለማስመለስ በቂ የታማኝነት ነጥቦች የሉዎትም,
 You have already assessed for the assessment criteria {}.,ቀድሞውንም ግምገማ መስፈርት ከገመገምን {}.,
 You have already selected items from {0} {1},ከዚህ ቀደም ከ ንጥሎች ተመርጠዋል ሊሆን {0} {1},
 You have been invited to collaborate on the project: {0},እርስዎ ፕሮጀክት ላይ ተባበር ተጋብዘዋል: {0},
diff --git a/erpnext/translations/ar.csv b/erpnext/translations/ar.csv
index 17d4386..550b5f2 100644
--- a/erpnext/translations/ar.csv
+++ b/erpnext/translations/ar.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',لا يمكنك حذف مشروع من نوع 'خارجي',
 You cannot edit root node.,لا يمكنك تحرير عقدة الجذر.,
 You cannot restart a Subscription that is not cancelled.,لا يمكنك إعادة تشغيل اشتراك غير ملغى.,
-You don't have enought Loyalty Points to redeem,ليس لديك ما يكفي من نقاط الولاء لاستردادها,
+You don't have enough Loyalty Points to redeem,ليس لديك ما يكفي من نقاط الولاء لاستردادها,
 You have already assessed for the assessment criteria {}.,لقد سبق أن قيمت معايير التقييم {}.,
 You have already selected items from {0} {1},لقد حددت العناصر من {0} {1},
 You have been invited to collaborate on the project: {0},لقد وجهت الدعوة إلى التعاون في هذا المشروع: {0},
diff --git a/erpnext/translations/bg.csv b/erpnext/translations/bg.csv
index 5fc10c4..baee526 100644
--- a/erpnext/translations/bg.csv
+++ b/erpnext/translations/bg.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Не можете да изтриете Тип на проекта &quot;Външен&quot;,
 You cannot edit root node.,Не можете да редактирате корен възел.,
 You cannot restart a Subscription that is not cancelled.,"Не можете да рестартирате абонамент, който не е анулиран.",
-You don't have enought Loyalty Points to redeem,"Нямате достатъчно точки за лоялност, за да осребрите",
+You don't have enough Loyalty Points to redeem,"Нямате достатъчно точки за лоялност, за да осребрите",
 You have already assessed for the assessment criteria {}.,Вече оценихте критериите за оценка {}.,
 You have already selected items from {0} {1},Вие вече сте избрали елементи от {0} {1},
 You have been invited to collaborate on the project: {0},Вие сте били поканени да си сътрудничат по проекта: {0},
diff --git a/erpnext/translations/bn.csv b/erpnext/translations/bn.csv
index 1da9bb6..266bd16 100644
--- a/erpnext/translations/bn.csv
+++ b/erpnext/translations/bn.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',আপনি প্রকল্প প্রকার &#39;বহিরাগত&#39; মুছে ফেলতে পারবেন না,
 You cannot edit root node.,আপনি রুট নোড সম্পাদনা করতে পারবেন না।,
 You cannot restart a Subscription that is not cancelled.,আপনি সাবস্ক্রিপশনটি বাতিল না করা পুনরায় শুরু করতে পারবেন না,
-You don't have enought Loyalty Points to redeem,আপনি বিক্রি করার জন্য আনুগত্য পয়েন্ট enought না,
+You don't have enough Loyalty Points to redeem,আপনি বিক্রি করার জন্য আনুগত্য পয়েন্ট enough না,
 You have already assessed for the assessment criteria {}.,"আপনি ইতিমধ্যে মূল্যায়ন মানদণ্ডের জন্য মূল্যায়ন করে নিলে, {}।",
 You have already selected items from {0} {1},আপনি ইতিমধ্যে থেকে আইটেম নির্বাচন করা আছে {0} {1},
 You have been invited to collaborate on the project: {0},আপনি প্রকল্পের সহযোগীতা করার জন্য আমন্ত্রণ জানানো হয়েছে: {0},
diff --git a/erpnext/translations/bs.csv b/erpnext/translations/bs.csv
index cab9c83..53e9d93 100644
--- a/erpnext/translations/bs.csv
+++ b/erpnext/translations/bs.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Ne možete obrisati tip projekta &#39;Spoljni&#39;,
 You cannot edit root node.,Ne možete uređivati root čvor.,
 You cannot restart a Subscription that is not cancelled.,Ne možete ponovo pokrenuti pretplatu koja nije otkazana.,
-You don't have enought Loyalty Points to redeem,Ne iskoristite Loyalty Points za otkup,
+You don't have enough Loyalty Points to redeem,Ne iskoristite Loyalty Points za otkup,
 You have already assessed for the assessment criteria {}.,Ste već ocijenili za kriterije procjene {}.,
 You have already selected items from {0} {1},Vi ste već odabrane stavke iz {0} {1},
 You have been invited to collaborate on the project: {0},Vi ste pozvani da surađuju na projektu: {0},
diff --git a/erpnext/translations/ca.csv b/erpnext/translations/ca.csv
index 0e16a74..4ca1435 100644
--- a/erpnext/translations/ca.csv
+++ b/erpnext/translations/ca.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',No es pot eliminar el tipus de projecte &#39;Extern&#39;,
 You cannot edit root node.,No podeu editar el node arrel.,
 You cannot restart a Subscription that is not cancelled.,No podeu reiniciar una subscripció que no es cancel·la.,
-You don't have enought Loyalty Points to redeem,No teniu punts de fidelització previstos per bescanviar,
+You don't have enough Loyalty Points to redeem,No teniu punts de fidelització previstos per bescanviar,
 You have already assessed for the assessment criteria {}.,Vostè ja ha avaluat pels criteris d&#39;avaluació {}.,
 You have already selected items from {0} {1},Ja ha seleccionat articles de {0} {1},
 You have been invited to collaborate on the project: {0},Se li ha convidat a col·laborar en el projecte: {0},
diff --git a/erpnext/translations/cs.csv b/erpnext/translations/cs.csv
index 3cef0de..26b8bf1 100644
--- a/erpnext/translations/cs.csv
+++ b/erpnext/translations/cs.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Nelze odstranit typ projektu &quot;Externí&quot;,
 You cannot edit root node.,Nelze upravit kořenový uzel.,
 You cannot restart a Subscription that is not cancelled.,"Nelze znovu spustit odběr, který není zrušen.",
-You don't have enought Loyalty Points to redeem,Nemáte dostatečné věrnostní body k uplatnění,
+You don't have enough Loyalty Points to redeem,Nemáte dostatečné věrnostní body k uplatnění,
 You have already assessed for the assessment criteria {}.,Již jste hodnotili kritéria hodnocení {}.,
 You have already selected items from {0} {1},Již jste vybrané položky z {0} {1},
 You have been invited to collaborate on the project: {0},Byli jste pozváni ke spolupráci na projektu: {0},
diff --git a/erpnext/translations/da.csv b/erpnext/translations/da.csv
index c58065a..09aaa15 100644
--- a/erpnext/translations/da.csv
+++ b/erpnext/translations/da.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Du kan ikke slette Project Type 'Ekstern',
 You cannot edit root node.,Du kan ikke redigere root node.,
 You cannot restart a Subscription that is not cancelled.,"Du kan ikke genstarte en abonnement, der ikke annulleres.",
-You don't have enought Loyalty Points to redeem,Du har ikke nok loyalitetspoint til at indløse,
+You don't have enough Loyalty Points to redeem,Du har ikke nok loyalitetspoint til at indløse,
 You have already assessed for the assessment criteria {}.,Du har allerede vurderet for bedømmelseskriterierne {}.,
 You have already selected items from {0} {1},Du har allerede valgt elementer fra {0} {1},
 You have been invited to collaborate on the project: {0},Du er blevet inviteret til at samarbejde om sag: {0},
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index 04e317a..e2c7467 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -3107,7 +3107,7 @@
 You cannot delete Project Type 'External',Sie können den Projekttyp &#39;Extern&#39; nicht löschen,
 You cannot edit root node.,Sie können den Stammknoten nicht bearbeiten.,
 You cannot restart a Subscription that is not cancelled.,Sie können ein nicht abgebrochenes Abonnement nicht neu starten.,
-You don't have enought Loyalty Points to redeem,Sie haben nicht genügend Treuepunkte zum Einlösen,
+You don't have enough Loyalty Points to redeem,Sie haben nicht genügend Treuepunkte zum Einlösen,
 You have already assessed for the assessment criteria {}.,Sie haben bereits für die Bewertungskriterien beurteilt.,
 You have already selected items from {0} {1},Sie haben bereits Elemente aus {0} {1} gewählt,
 You have been invited to collaborate on the project: {0},Sie wurden zur Zusammenarbeit für das Projekt {0} eingeladen.,
diff --git a/erpnext/translations/el.csv b/erpnext/translations/el.csv
index 9d15e61..ae72c26 100644
--- a/erpnext/translations/el.csv
+++ b/erpnext/translations/el.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Δεν μπορείτε να διαγράψετε τον τύπο έργου &#39;Εξωτερικό&#39;,
 You cannot edit root node.,Δεν μπορείτε να επεξεργαστείτε τον κόμβο ρίζας.,
 You cannot restart a Subscription that is not cancelled.,Δεν μπορείτε να κάνετε επανεκκίνηση μιας συνδρομής που δεν ακυρώνεται.,
-You don't have enought Loyalty Points to redeem,Δεν διαθέτετε σημεία αφοσίωσης για εξαργύρωση,
+You don't have enough Loyalty Points to redeem,Δεν διαθέτετε σημεία αφοσίωσης για εξαργύρωση,
 You have already assessed for the assessment criteria {}.,Έχετε ήδη αξιολογήσει τα κριτήρια αξιολόγησης {}.,
 You have already selected items from {0} {1},Έχετε ήδη επιλεγμένα αντικείμενα από {0} {1},
 You have been invited to collaborate on the project: {0},Έχετε προσκληθεί να συνεργαστούν για το έργο: {0},
diff --git a/erpnext/translations/es.csv b/erpnext/translations/es.csv
index 50074d2..d931429 100644
--- a/erpnext/translations/es.csv
+++ b/erpnext/translations/es.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',No puede eliminar Tipo de proyecto &#39;Externo&#39;,
 You cannot edit root node.,No puedes editar el nodo raíz.,
 You cannot restart a Subscription that is not cancelled.,No puede reiniciar una suscripción que no está cancelada.,
-You don't have enought Loyalty Points to redeem,No tienes suficientes puntos de lealtad para canjear,
+You don't have enough Loyalty Points to redeem,No tienes suficientes puntos de lealtad para canjear,
 You have already assessed for the assessment criteria {}.,Ya ha evaluado los criterios de evaluación {}.,
 You have already selected items from {0} {1},Ya ha seleccionado artículos de {0} {1},
 You have been invited to collaborate on the project: {0},Se le ha invitado a colaborar en el proyecto: {0},
diff --git a/erpnext/translations/et.csv b/erpnext/translations/et.csv
index a9f6c6c..29e599b 100644
--- a/erpnext/translations/et.csv
+++ b/erpnext/translations/et.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Te ei saa projekti tüübi &quot;Väline&quot; kustutada,
 You cannot edit root node.,Sa ei saa redigeerida juursõlme.,
 You cannot restart a Subscription that is not cancelled.,Te ei saa tellimust uuesti katkestada.,
-You don't have enought Loyalty Points to redeem,"Teil pole lojaalsuspunkte, mida soovite lunastada",
+You don't have enough Loyalty Points to redeem,"Teil pole lojaalsuspunkte, mida soovite lunastada",
 You have already assessed for the assessment criteria {}.,Olete juba hinnanud hindamise kriteeriumid {}.,
 You have already selected items from {0} {1},Olete juba valitud objektide {0} {1},
 You have been invited to collaborate on the project: {0},Sind on kutsutud koostööd projekti: {0},
diff --git a/erpnext/translations/fa.csv b/erpnext/translations/fa.csv
index 35a42e8..4c5ab80 100644
--- a/erpnext/translations/fa.csv
+++ b/erpnext/translations/fa.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',شما نمیتوانید نوع پروژه «خارجی» را حذف کنید,
 You cannot edit root node.,نمی توانید گره ریشه را ویرایش کنید,
 You cannot restart a Subscription that is not cancelled.,شما نمی توانید اشتراک را لغو کنید.,
-You don't have enought Loyalty Points to redeem,شما نمیتوانید امتیازات وفاداری خود را به دست آورید,
+You don't have enough Loyalty Points to redeem,شما نمیتوانید امتیازات وفاداری خود را به دست آورید,
 You have already assessed for the assessment criteria {}.,شما در حال حاضر برای معیارهای ارزیابی ارزیابی {}.,
 You have already selected items from {0} {1},شما در حال حاضر اقلام از انتخاب {0} {1},
 You have been invited to collaborate on the project: {0},از شما دعوت شده برای همکاری در این پروژه: {0},
diff --git a/erpnext/translations/fi.csv b/erpnext/translations/fi.csv
index 9d7bf8b..c26441b 100644
--- a/erpnext/translations/fi.csv
+++ b/erpnext/translations/fi.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Et voi poistaa projektityyppiä &quot;Ulkoinen&quot;,
 You cannot edit root node.,Et voi muokata juurisolmua.,
 You cannot restart a Subscription that is not cancelled.,"Et voi uudelleenkäynnistää tilausta, jota ei peruuteta.",
-You don't have enought Loyalty Points to redeem,Sinulla ei ole tarpeeksi Loyalty Pointsia lunastettavaksi,
+You don't have enough Loyalty Points to redeem,Sinulla ei ole tarpeeksi Loyalty Pointsia lunastettavaksi,
 You have already assessed for the assessment criteria {}.,Olet jo arvioitu arviointikriteerit {}.,
 You have already selected items from {0} {1},Olet jo valitut kohteet {0} {1},
 You have been invited to collaborate on the project: {0},Sinut on kutsuttu yhteistyöhön projektissa {0},
diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv
index 6d5505b..801604a 100644
--- a/erpnext/translations/fr.csv
+++ b/erpnext/translations/fr.csv
@@ -3093,7 +3093,7 @@
 You cannot delete Project Type 'External',Vous ne pouvez pas supprimer le Type de Projet 'Externe',
 You cannot edit root node.,Vous ne pouvez pas modifier le nœud racine.,
 You cannot restart a Subscription that is not cancelled.,Vous ne pouvez pas redémarrer un abonnement qui n'est pas annulé.,
-You don't have enought Loyalty Points to redeem,Vous n'avez pas assez de points de fidélité à échanger,
+You don't have enough Loyalty Points to redeem,Vous n'avez pas assez de points de fidélité à échanger,
 You have already assessed for the assessment criteria {}.,Vous avez déjà évalué les critères d'évaluation {}.,
 You have already selected items from {0} {1},Vous avez déjà choisi des articles de {0} {1},
 You have been invited to collaborate on the project: {0},Vous avez été invité à collaborer sur le projet : {0},
diff --git a/erpnext/translations/gu.csv b/erpnext/translations/gu.csv
index 025ec89..5691597 100644
--- a/erpnext/translations/gu.csv
+++ b/erpnext/translations/gu.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',તમે &#39;બાહ્ય&#39; પ્રોજેક્ટ પ્રકારને કાઢી શકતા નથી,
 You cannot edit root node.,તમે રૂટ નોડને સંપાદિત કરી શકતા નથી.,
 You cannot restart a Subscription that is not cancelled.,તમે સબ્સ્ક્રિપ્શન ફરીથી શરૂ કરી શકતા નથી કે જે રદ કરવામાં આવી નથી.,
-You don't have enought Loyalty Points to redeem,તમારી પાસે રિડીમ કરવા માટે વફાદારીના પોઇંટ્સ નથી,
+You don't have enough Loyalty Points to redeem,તમારી પાસે રિડીમ કરવા માટે વફાદારીના પોઇંટ્સ નથી,
 You have already assessed for the assessment criteria {}.,જો તમે પહેલાથી જ આકારણી માપદંડ માટે આકારણી છે {}.,
 You have already selected items from {0} {1},જો તમે પહેલાથી જ વસ્તુઓ પસંદ કરેલ {0} {1},
 You have been invited to collaborate on the project: {0},તમે આ પ્રોજેક્ટ પર સહયોગ કરવા માટે આમંત્રિત કરવામાં આવ્યા છે: {0},
diff --git a/erpnext/translations/he.csv b/erpnext/translations/he.csv
index 43bac41..9aa152c 100644
--- a/erpnext/translations/he.csv
+++ b/erpnext/translations/he.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',אינך יכול למחוק את סוג הפרויקט &#39;חיצוני&#39;,
 You cannot edit root node.,אינך יכול לערוך צומת שורש.,
 You cannot restart a Subscription that is not cancelled.,אינך יכול להפעיל מחדש מנוי שאינו מבוטל.,
-You don't have enought Loyalty Points to redeem,אין לך מספיק נקודות נאמנות למימוש,
+You don't have enough Loyalty Points to redeem,אין לך מספיק נקודות נאמנות למימוש,
 You have already assessed for the assessment criteria {}.,כבר הערכת את קריטריוני ההערכה {}.,
 You have already selected items from {0} {1},בחרת כבר פריטים מ- {0} {1},
 You have been invited to collaborate on the project: {0},הוזמנת לשתף פעולה על הפרויקט: {0},
diff --git a/erpnext/translations/hi.csv b/erpnext/translations/hi.csv
index 00747d4..d56dcec 100644
--- a/erpnext/translations/hi.csv
+++ b/erpnext/translations/hi.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',आप परियोजना प्रकार &#39;बाहरी&#39; को नहीं हटा सकते,
 You cannot edit root node.,आप रूट नोड संपादित नहीं कर सकते हैं।,
 You cannot restart a Subscription that is not cancelled.,आप एक सदस्यता को पुनरारंभ नहीं कर सकते जो रद्द नहीं किया गया है।,
-You don't have enought Loyalty Points to redeem,आपने रिडीम करने के लिए वफादारी अंक नहीं खरीदे हैं,
+You don't have enough Loyalty Points to redeem,आपने रिडीम करने के लिए वफादारी अंक नहीं खरीदे हैं,
 You have already assessed for the assessment criteria {}.,आप मूल्यांकन मानदंड के लिए पहले से ही मूल्यांकन कर चुके हैं {},
 You have already selected items from {0} {1},आप पहले से ही से आइटम का चयन किया है {0} {1},
 You have been invited to collaborate on the project: {0},आप इस परियोजना पर सहयोग करने के लिए आमंत्रित किया गया है: {0},
diff --git a/erpnext/translations/hr.csv b/erpnext/translations/hr.csv
index ec24026..827ae2c 100644
--- a/erpnext/translations/hr.csv
+++ b/erpnext/translations/hr.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Ne možete izbrisati vrstu projekta &#39;Vanjski&#39;,
 You cannot edit root node.,Ne možete uređivati root čvor.,
 You cannot restart a Subscription that is not cancelled.,Ne možete ponovo pokrenuti pretplatu koja nije otkazana.,
-You don't have enought Loyalty Points to redeem,Nemate dovoljno bodova lojalnosti za otkup,
+You don't have enough Loyalty Points to redeem,Nemate dovoljno bodova lojalnosti za otkup,
 You have already assessed for the assessment criteria {}.,Već ste ocijenili kriterije procjene {}.,
 You have already selected items from {0} {1},Već ste odabrali stavke iz {0} {1},
 You have been invited to collaborate on the project: {0},Pozvani ste za suradnju na projektu: {0},
diff --git a/erpnext/translations/hu.csv b/erpnext/translations/hu.csv
index f92946a..e68b56f 100644
--- a/erpnext/translations/hu.csv
+++ b/erpnext/translations/hu.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',"A ""Külső"" projekttípust nem törölheti",
 You cannot edit root node.,Nem szerkesztheti a fő csomópontot.,
 You cannot restart a Subscription that is not cancelled.,"Nem indíthatja el az Előfizetést, amelyet nem zárt le.",
-You don't have enought Loyalty Points to redeem,Nincs elegendő hűségpontjaid megváltáshoz,
+You don't have enough Loyalty Points to redeem,Nincs elegendő hűségpontjaid megváltáshoz,
 You have already assessed for the assessment criteria {}.,Már értékelte ezekkel az értékelési kritériumokkal: {}.,
 You have already selected items from {0} {1},Már választott ki elemeket innen {0} {1},
 You have been invited to collaborate on the project: {0},Ön meghívást kapott ennek a projeknek a közreműködéséhez: {0},
diff --git a/erpnext/translations/id.csv b/erpnext/translations/id.csv
index 8ba9e66..67311a1 100644
--- a/erpnext/translations/id.csv
+++ b/erpnext/translations/id.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Anda tidak bisa menghapus Jenis Proyek 'External',
 You cannot edit root node.,Anda tidak dapat mengedit simpul root.,
 You cannot restart a Subscription that is not cancelled.,Anda tidak dapat memulai ulang Langganan yang tidak dibatalkan.,
-You don't have enought Loyalty Points to redeem,Anda tidak memiliki Poin Loyalitas yang cukup untuk ditukarkan,
+You don't have enough Loyalty Points to redeem,Anda tidak memiliki Poin Loyalitas yang cukup untuk ditukarkan,
 You have already assessed for the assessment criteria {}.,Anda telah memberikan penilaian terhadap kriteria penilaian {}.,
 You have already selected items from {0} {1},Anda sudah memilih item dari {0} {1},
 You have been invited to collaborate on the project: {0},Anda telah diundang untuk berkolaborasi pada proyek: {0},
diff --git a/erpnext/translations/is.csv b/erpnext/translations/is.csv
index 9802592..1bf6b4c 100644
--- a/erpnext/translations/is.csv
+++ b/erpnext/translations/is.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Þú getur ekki eytt verkefnisgerðinni &#39;ytri&#39;,
 You cannot edit root node.,Þú getur ekki breytt rótarkóði.,
 You cannot restart a Subscription that is not cancelled.,Þú getur ekki endurræst áskrift sem ekki er lokað.,
-You don't have enought Loyalty Points to redeem,Þú hefur ekki nóg hollusta stig til að innleysa,
+You don't have enough Loyalty Points to redeem,Þú hefur ekki nóg hollusta stig til að innleysa,
 You have already assessed for the assessment criteria {}.,Þú hefur nú þegar metið mat á viðmiðunum {}.,
 You have already selected items from {0} {1},Þú hefur nú þegar valið hluti úr {0} {1},
 You have been invited to collaborate on the project: {0},Þér hefur verið boðið að vinna að verkefninu: {0},
diff --git a/erpnext/translations/it.csv b/erpnext/translations/it.csv
index 064f16b..f96b1aa 100644
--- a/erpnext/translations/it.csv
+++ b/erpnext/translations/it.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Non è possibile eliminare il tipo di progetto &#39;Esterno&#39;,
 You cannot edit root node.,Non è possibile modificare il nodo principale.,
 You cannot restart a Subscription that is not cancelled.,Non è possibile riavviare una sottoscrizione che non è stata annullata.,
-You don't have enought Loyalty Points to redeem,Non hai abbastanza Punti fedeltà da riscattare,
+You don't have enough Loyalty Points to redeem,Non hai abbastanza Punti fedeltà da riscattare,
 You have already assessed for the assessment criteria {}.,Hai già valutato i criteri di valutazione {}.,
 You have already selected items from {0} {1},Hai già selezionato elementi da {0} {1},
 You have been invited to collaborate on the project: {0},Sei stato invitato a collaborare al progetto: {0},
diff --git a/erpnext/translations/ja.csv b/erpnext/translations/ja.csv
index b5064f6..e5ebeb3 100644
--- a/erpnext/translations/ja.csv
+++ b/erpnext/translations/ja.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',プロジェクトタイプ「外部」を削除することはできません,
 You cannot edit root node.,ルートノードは編集できません。,
 You cannot restart a Subscription that is not cancelled.,キャンセルされていないサブスクリプションを再起動することはできません。,
-You don't have enought Loyalty Points to redeem,あなたは交換するのに十分なロイヤリティポイントがありません,
+You don't have enough Loyalty Points to redeem,あなたは交換するのに十分なロイヤリティポイントがありません,
 You have already assessed for the assessment criteria {}.,評価基準{}は評価済です。,
 You have already selected items from {0} {1},項目を選択済みです {0} {1},
 You have been invited to collaborate on the project: {0},プロジェクト:{0} の共同作業に招待されました,
diff --git a/erpnext/translations/km.csv b/erpnext/translations/km.csv
index 31dcdc0..0dbecca 100644
--- a/erpnext/translations/km.csv
+++ b/erpnext/translations/km.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',អ្នកមិនអាចលុបប្រភេទគម្រោង &#39;ខាងក្រៅ&#39;,
 You cannot edit root node.,អ្នកមិនអាចកែថ្នាំង root បានទេ។,
 You cannot restart a Subscription that is not cancelled.,អ្នកមិនអាចចាប់ផ្តើមឡើងវិញនូវការជាវដែលមិនត្រូវបានលុបចោលទេ។,
-You don't have enought Loyalty Points to redeem,អ្នកមិនមានពិន្ទុភាពស្មោះត្រង់គ្រប់គ្រាន់ដើម្បីលោះទេ,
+You don't have enough Loyalty Points to redeem,អ្នកមិនមានពិន្ទុភាពស្មោះត្រង់គ្រប់គ្រាន់ដើម្បីលោះទេ,
 You have already assessed for the assessment criteria {}.,អ្នកបានវាយតម្លែរួចទៅហើយសម្រាប់លក្ខណៈវិនិច្ឆ័យវាយតម្លៃនេះ {} ។,
 You have already selected items from {0} {1},អ្នកបានជ្រើសរួចហើយចេញពីធាតុ {0} {1},
 You have been invited to collaborate on the project: {0},អ្នកបានត្រូវអញ្ជើញដើម្បីសហការគ្នាលើគម្រោងនេះ: {0},
diff --git a/erpnext/translations/kn.csv b/erpnext/translations/kn.csv
index f01e386..b929e29 100644
--- a/erpnext/translations/kn.csv
+++ b/erpnext/translations/kn.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',ನೀವು ಪ್ರಾಜೆಕ್ಟ್ ಕೌಟುಂಬಿಕತೆ &#39;ಬಾಹ್ಯ&#39; ಅನ್ನು ಅಳಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ,
 You cannot edit root node.,ನೀವು ರೂಟ್ ನೋಡ್ ಅನ್ನು ಸಂಪಾದಿಸಲಾಗುವುದಿಲ್ಲ.,
 You cannot restart a Subscription that is not cancelled.,ರದ್ದುಪಡಿಸದ ಚಂದಾದಾರಿಕೆಯನ್ನು ನೀವು ಮರುಪ್ರಾರಂಭಿಸಬಾರದು.,
-You don't have enought Loyalty Points to redeem,ರಿಡೀಮ್ ಮಾಡಲು ನೀವು ಲಾಯಲ್ಟಿ ಪಾಯಿಂಟುಗಳನ್ನು ಹೊಂದಿದ್ದೀರಿ,
+You don't have enough Loyalty Points to redeem,ರಿಡೀಮ್ ಮಾಡಲು ನೀವು ಲಾಯಲ್ಟಿ ಪಾಯಿಂಟುಗಳನ್ನು ಹೊಂದಿದ್ದೀರಿ,
 You have already assessed for the assessment criteria {}.,ನೀವು ಈಗಾಗಲೇ ಮೌಲ್ಯಮಾಪನ ಮಾನದಂಡದ ನಿರ್ಣಯಿಸುವ {}.,
 You have already selected items from {0} {1},ನೀವು ಈಗಾಗಲೇ ಆಯ್ಕೆ ಐಟಂಗಳನ್ನು ಎಂದು {0} {1},
 You have been invited to collaborate on the project: {0},ನೀವು ಯೋಜನೆಯ ಸಹಯೋಗಿಸಲು ಆಮಂತ್ರಿಸಲಾಗಿದೆ: {0},
diff --git a/erpnext/translations/ko.csv b/erpnext/translations/ko.csv
index dffcaa8..1c8020f 100644
--- a/erpnext/translations/ko.csv
+++ b/erpnext/translations/ko.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',프로젝트 유형 &#39;외부&#39;를 삭제할 수 없습니다.,
 You cannot edit root node.,루트 노드는 편집 할 수 없습니다.,
 You cannot restart a Subscription that is not cancelled.,취소되지 않은 구독은 다시 시작할 수 없습니다.,
-You don't have enought Loyalty Points to redeem,사용하기에 충성도 포인트가 충분하지 않습니다.,
+You don't have enough Loyalty Points to redeem,사용하기에 충성도 포인트가 충분하지 않습니다.,
 You have already assessed for the assessment criteria {}.,이미 평가 기준 {}을 (를) 평가했습니다.,
 You have already selected items from {0} {1},이미에서 항목을 선택한 {0} {1},
 You have been invited to collaborate on the project: {0},당신은 프로젝트 공동 작업에 초대되었습니다 : {0},
diff --git a/erpnext/translations/ku.csv b/erpnext/translations/ku.csv
index 047ee89..f4d1197 100644
--- a/erpnext/translations/ku.csv
+++ b/erpnext/translations/ku.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Hûn nikarin jêbirinê hilbijêre &#39;External&#39;,
 You cannot edit root node.,Hûn nikarin node root root biguherînin.,
 You cannot restart a Subscription that is not cancelled.,Hûn nikarin endamê peymana ku destûr nabe.,
-You don't have enought Loyalty Points to redeem,Hûn pisporên dilsozî ne ku hûn bistînin,
+You don't have enough Loyalty Points to redeem,Hûn pisporên dilsozî ne ku hûn bistînin,
 You have already assessed for the assessment criteria {}.,Tu niha ji bo nirxandina nirxandin {}.,
 You have already selected items from {0} {1},Jixwe te tomar ji hilbijartî {0} {1},
 You have been invited to collaborate on the project: {0},Hûn hatine vexwendin ji bo hevkariyê li ser vê projeyê: {0},
diff --git a/erpnext/translations/lo.csv b/erpnext/translations/lo.csv
index c94bc25..9e77b51 100644
--- a/erpnext/translations/lo.csv
+++ b/erpnext/translations/lo.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',ທ່ານບໍ່ສາມາດລຶບປະເພດໂຄງການ &#39;ພາຍນອກ&#39;,
 You cannot edit root node.,ທ່ານບໍ່ສາມາດແກ້ໄຂຮາກຮາກ.,
 You cannot restart a Subscription that is not cancelled.,ທ່ານບໍ່ສາມາດເລີ່ມຕົ້ນລະບົບຈອງໃຫມ່ທີ່ບໍ່ໄດ້ຖືກຍົກເລີກ.,
-You don't have enought Loyalty Points to redeem,ທ່ານບໍ່ມີຈຸດປະສົງອັນຄົບຖ້ວນພໍທີ່ຈະຊື້,
+You don't have enough Loyalty Points to redeem,ທ່ານບໍ່ມີຈຸດປະສົງອັນຄົບຖ້ວນພໍທີ່ຈະຊື້,
 You have already assessed for the assessment criteria {}.,ທ່ານໄດ້ປະເມີນແລ້ວສໍາລັບມາດຕະຖານການປະເມີນຜົນ {}.,
 You have already selected items from {0} {1},ທ່ານໄດ້ຄັດເລືອກເອົາແລ້ວລາຍການຈາກ {0} {1},
 You have been invited to collaborate on the project: {0},ທ່ານໄດ້ຖືກເຊື້ອເຊີນເພື່ອເຮັດວຽກຮ່ວມກັນກ່ຽວກັບໂຄງການ: {0},
diff --git a/erpnext/translations/lt.csv b/erpnext/translations/lt.csv
index 731638c..66215c1 100644
--- a/erpnext/translations/lt.csv
+++ b/erpnext/translations/lt.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Negalite ištrinti projekto tipo &quot;Išorinis&quot;,
 You cannot edit root node.,Jūs negalite redaguoti šakninis mazgas.,
 You cannot restart a Subscription that is not cancelled.,"Jūs negalite iš naujo paleisti Prenumeratos, kuri nėra atšaukta.",
-You don't have enought Loyalty Points to redeem,Jūs neturite nusipirkti lojalumo taškų išpirkti,
+You don't have enough Loyalty Points to redeem,Jūs neturite nusipirkti lojalumo taškų išpirkti,
 You have already assessed for the assessment criteria {}.,Jūs jau įvertintas vertinimo kriterijus {}.,
 You have already selected items from {0} {1},Jūs jau pasirinkote elementus iš {0} {1},
 You have been invited to collaborate on the project: {0},Jūs buvote pakviestas bendradarbiauti su projektu: {0},
diff --git a/erpnext/translations/lv.csv b/erpnext/translations/lv.csv
index 71b51f4..1409719 100644
--- a/erpnext/translations/lv.csv
+++ b/erpnext/translations/lv.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Jūs nevarat izdzēst projekta veidu &quot;Ārējais&quot;,
 You cannot edit root node.,Jūs nevarat rediģēt saknes mezglu.,
 You cannot restart a Subscription that is not cancelled.,"Jūs nevarat atsākt Abonementu, kas nav atcelts.",
-You don't have enought Loyalty Points to redeem,Jums nav lojalitātes punktu atpirkt,
+You don't have enough Loyalty Points to redeem,Jums nav lojalitātes punktu atpirkt,
 You have already assessed for the assessment criteria {}.,Jūs jau izvērtēta vērtēšanas kritērijiem {}.,
 You have already selected items from {0} {1},Jūs jau atsevišķus posteņus {0} {1},
 You have been invited to collaborate on the project: {0},Jūs esat uzaicināts sadarboties projektam: {0},
diff --git a/erpnext/translations/mk.csv b/erpnext/translations/mk.csv
index 2dcef0c..06d7e65 100644
--- a/erpnext/translations/mk.csv
+++ b/erpnext/translations/mk.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Не можете да го избришете Типот на проектот &#39;External&#39;,
 You cannot edit root node.,Не можете да уредувате корен јазол.,
 You cannot restart a Subscription that is not cancelled.,Не можете да ја рестартирате претплатата која не е откажана.,
-You don't have enought Loyalty Points to redeem,Вие не сте донеле лојални точки за откуп,
+You don't have enough Loyalty Points to redeem,Вие не сте донеле лојални точки за откуп,
 You have already assessed for the assessment criteria {}.,Веќе сте се проценува за критериумите за оценување {}.,
 You have already selected items from {0} {1},Веќе сте одбрале предмети од {0} {1},
 You have been invited to collaborate on the project: {0},Вие сте поканети да соработуваат на проектот: {0},
diff --git a/erpnext/translations/ml.csv b/erpnext/translations/ml.csv
index 46629a3..22684b6 100644
--- a/erpnext/translations/ml.csv
+++ b/erpnext/translations/ml.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',നിങ്ങൾക്ക് പദ്ധതി തരം &#39;ബാഹ്യ&#39; ഇല്ലാതാക്കാൻ കഴിയില്ല,
 You cannot edit root node.,നിങ്ങൾക്ക് റൂട്ട് നോഡ് എഡിറ്റുചെയ്യാൻ കഴിയില്ല.,
 You cannot restart a Subscription that is not cancelled.,നിങ്ങൾക്ക് റദ്ദാക്കാത്ത ഒരു സബ്സ്ക്രിപ്ഷൻ പുനഃരാരംഭിക്കാൻ കഴിയില്ല.,
-You don't have enought Loyalty Points to redeem,നിങ്ങൾക്ക് വീണ്ടെടുക്കാനുള്ള വിശ്വസ്ത ടയറുകൾ ആവശ്യമില്ല,
+You don't have enough Loyalty Points to redeem,നിങ്ങൾക്ക് വീണ്ടെടുക്കാനുള്ള വിശ്വസ്ത ടയറുകൾ ആവശ്യമില്ല,
 You have already assessed for the assessment criteria {}.,ഇതിനകം നിങ്ങൾ വിലയിരുത്തൽ മാനദണ്ഡങ്ങൾ {} വേണ്ടി വിലയിരുത്തി ചെയ്തു.,
 You have already selected items from {0} {1},നിങ്ങൾ ഇതിനകം നിന്ന് {0} {1} ഇനങ്ങൾ തിരഞ്ഞെടുത്തു,
 You have been invited to collaborate on the project: {0},നിങ്ങൾ പദ്ധതി സഹകരിക്കുക ക്ഷണിച്ചു: {0},
diff --git a/erpnext/translations/mr.csv b/erpnext/translations/mr.csv
index 7f1c5e2..87e0fdd 100644
--- a/erpnext/translations/mr.csv
+++ b/erpnext/translations/mr.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',आपण प्रोजेक्ट प्रकार &#39;बाह्य&#39; हटवू शकत नाही,
 You cannot edit root node.,आपण मूळ नोड संपादित करू शकत नाही.,
 You cannot restart a Subscription that is not cancelled.,आपण रद्द न केलेली सबस्क्रिप्शन पुन्हा सुरू करू शकत नाही.,
-You don't have enought Loyalty Points to redeem,आपण परत विकत घेण्यासाठी निष्ठावान बिंदू नाहीत,
+You don't have enough Loyalty Points to redeem,आपण परत विकत घेण्यासाठी निष्ठावान बिंदू नाहीत,
 You have already assessed for the assessment criteria {}.,आपण मूल्यांकन निकष आधीच मूल्यमापन आहे {}.,
 You have already selected items from {0} {1},आपण आधीच आयटम निवडले आहेत {0} {1},
 You have been invited to collaborate on the project: {0},आपण प्रकल्प सहयोग करण्यासाठी आमंत्रित आहेत: {0},
diff --git a/erpnext/translations/ms.csv b/erpnext/translations/ms.csv
index 0dfc55b..8e1ac51 100644
--- a/erpnext/translations/ms.csv
+++ b/erpnext/translations/ms.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Anda tidak boleh memadam Jenis Projek &#39;Luar&#39;,
 You cannot edit root node.,Anda tidak boleh mengedit nod akar.,
 You cannot restart a Subscription that is not cancelled.,Anda tidak boleh memulakan semula Langganan yang tidak dibatalkan.,
-You don't have enought Loyalty Points to redeem,Anda tidak mempunyai mata Kesetiaan yang cukup untuk menebusnya,
+You don't have enough Loyalty Points to redeem,Anda tidak mempunyai mata Kesetiaan yang cukup untuk menebusnya,
 You have already assessed for the assessment criteria {}.,Anda telah pun dinilai untuk kriteria penilaian {}.,
 You have already selected items from {0} {1},Anda telah memilih barangan dari {0} {1},
 You have been invited to collaborate on the project: {0},Anda telah dijemput untuk bekerjasama dalam projek: {0},
diff --git a/erpnext/translations/my.csv b/erpnext/translations/my.csv
index c0c0c45..6da5860 100644
--- a/erpnext/translations/my.csv
+++ b/erpnext/translations/my.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',သငျသညျစီမံကိန်းအမျိုးအစား &#39;&#39; ပြင်ပ &#39;&#39; မဖျက်နိုင်ပါ,
 You cannot edit root node.,သငျသညျအမြစ် node ကိုတည်းဖြတ်မရနိုင်ပါ။,
 You cannot restart a Subscription that is not cancelled.,သငျသညျဖျက်သိမ်းမပေးကြောင်းတစ် Subscription ပြန်လည်စတင်ရန်လို့မရပါဘူး။,
-You don't have enought Loyalty Points to redeem,သငျသညျကိုရှေးနှုတျမှ enought သစ္စာရှိမှုအမှတ်ရှိသည်မဟုတ်ကြဘူး,
+You don't have enough Loyalty Points to redeem,သငျသညျကိုရှေးနှုတျမှ enough သစ္စာရှိမှုအမှတ်ရှိသည်မဟုတ်ကြဘူး,
 You have already assessed for the assessment criteria {}.,သငျသညျပြီးသား {} အဆိုပါအကဲဖြတ်သတ်မှတ်ချက်အဘို့အအကဲဖြတ်ပါပြီ။,
 You have already selected items from {0} {1},သငျသညျပြီးသား {0} {1} ကနေပစ္စည်းကိုရှေးခယျြခဲ့ကြ,
 You have been invited to collaborate on the project: {0},သငျသညျစီမံကိန်းကိုအပေါ်ပူးပေါင်းဖို့ဖိတ်ခေါ်ခဲ့ကြ: {0},
diff --git a/erpnext/translations/nl.csv b/erpnext/translations/nl.csv
index 4d81095..96d1770 100644
--- a/erpnext/translations/nl.csv
+++ b/erpnext/translations/nl.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',U kunt projecttype &#39;extern&#39; niet verwijderen,
 You cannot edit root node.,U kunt het basisknooppunt niet bewerken.,
 You cannot restart a Subscription that is not cancelled.,U kunt een Abonnement dat niet is geannuleerd niet opnieuw opstarten.,
-You don't have enought Loyalty Points to redeem,Je hebt geen genoeg loyaliteitspunten om in te wisselen,
+You don't have enough Loyalty Points to redeem,Je hebt geen genoeg loyaliteitspunten om in te wisselen,
 You have already assessed for the assessment criteria {}.,U heeft al beoordeeld op de beoordelingscriteria {}.,
 You have already selected items from {0} {1},U heeft reeds geselecteerde items uit {0} {1},
 You have been invited to collaborate on the project: {0},U bent uitgenodigd om mee te werken aan het project: {0},
diff --git a/erpnext/translations/no.csv b/erpnext/translations/no.csv
index 0ee6ed6..f285e48 100644
--- a/erpnext/translations/no.csv
+++ b/erpnext/translations/no.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Du kan ikke slette Project Type &#39;External&#39;,
 You cannot edit root node.,Du kan ikke redigere rotknutepunktet.,
 You cannot restart a Subscription that is not cancelled.,Du kan ikke starte en abonnement som ikke er kansellert.,
-You don't have enought Loyalty Points to redeem,Du har ikke nok lojalitetspoeng til å innløse,
+You don't have enough Loyalty Points to redeem,Du har ikke nok lojalitetspoeng til å innløse,
 You have already assessed for the assessment criteria {}.,Du har allerede vurdert for vurderingskriteriene {}.,
 You have already selected items from {0} {1},Du har allerede valgt elementer fra {0} {1},
 You have been invited to collaborate on the project: {0},Du har blitt invitert til å samarbeide om prosjektet: {0},
diff --git a/erpnext/translations/pl.csv b/erpnext/translations/pl.csv
index e0ecec5..82cc64d 100644
--- a/erpnext/translations/pl.csv
+++ b/erpnext/translations/pl.csv
@@ -3069,7 +3069,7 @@
 You cannot delete Project Type 'External',Nie można usunąć typu projektu &quot;zewnętrzny&quot;,
 You cannot edit root node.,Nie można edytować węzła głównego.,
 You cannot restart a Subscription that is not cancelled.,"Nie można ponownie uruchomić subskrypcji, która nie zostanie anulowana.",
-You don't have enought Loyalty Points to redeem,"Nie masz wystarczającej liczby Punktów Lojalnościowych, aby je wykorzystać",
+You don't have enough Loyalty Points to redeem,"Nie masz wystarczającej liczby Punktów Lojalnościowych, aby je wykorzystać",
 You have already assessed for the assessment criteria {}.,Oceniałeś już kryteria oceny {}.,
 You have already selected items from {0} {1},Już wybrane pozycje z {0} {1},
 You have been invited to collaborate on the project: {0},Zostałeś zaproszony do współpracy przy projekcie: {0},
diff --git a/erpnext/translations/ps.csv b/erpnext/translations/ps.csv
index 8788bcb..27df03c 100644
--- a/erpnext/translations/ps.csv
+++ b/erpnext/translations/ps.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',تاسو د پروژې ډول &#39;بهرني&#39; نه ړنګولی شئ,
 You cannot edit root node.,تاسو د ریډ نوډ سمون نشو کولی.,
 You cannot restart a Subscription that is not cancelled.,تاسو نشي کولی هغه یو بل ریکارډ بیا پیل کړئ چې رد شوی نه وي.,
-You don't have enought Loyalty Points to redeem,تاسو د ژغورلو لپاره د وفادارۍ ټکي نلرئ,
+You don't have enough Loyalty Points to redeem,تاسو د ژغورلو لپاره د وفادارۍ ټکي نلرئ,
 You have already assessed for the assessment criteria {}.,تاسو مخکې د ارزونې معیارونه ارزول {}.,
 You have already selected items from {0} {1},تاسو وخته ټاکل څخه توکي {0} د {1},
 You have been invited to collaborate on the project: {0},تاسو ته په دغه پروژه کې همکاري بلل شوي دي: {0},
diff --git a/erpnext/translations/pt-BR.csv b/erpnext/translations/pt-BR.csv
index 3aa00ba..c07082e 100644
--- a/erpnext/translations/pt-BR.csv
+++ b/erpnext/translations/pt-BR.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Você não pode excluir o Tipo de Projeto ';Externo';,
 You cannot edit root node.,Você não pode editar o nó raiz.,
 You cannot restart a Subscription that is not cancelled.,Você não pode reiniciar uma Assinatura que não seja cancelada.,
-You don't have enought Loyalty Points to redeem,Você não tem suficientes pontos de lealdade para resgatar,
+You don't have enough Loyalty Points to redeem,Você não tem suficientes pontos de lealdade para resgatar,
 You have already assessed for the assessment criteria {}.,Você já avaliou os critérios de avaliação {}.,
 You have already selected items from {0} {1},Já selecionou itens de {0} {1},
 You have been invited to collaborate on the project: {0},Você foi convidado para colaborar com o projeto: {0},
diff --git a/erpnext/translations/pt.csv b/erpnext/translations/pt.csv
index f52ed55..9b7a854 100644
--- a/erpnext/translations/pt.csv
+++ b/erpnext/translations/pt.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Você não pode excluir o Tipo de Projeto &#39;Externo&#39;,
 You cannot edit root node.,Você não pode editar o nó raiz.,
 You cannot restart a Subscription that is not cancelled.,Você não pode reiniciar uma Assinatura que não seja cancelada.,
-You don't have enought Loyalty Points to redeem,Você não tem suficientes pontos de lealdade para resgatar,
+You don't have enough Loyalty Points to redeem,Você não tem suficientes pontos de lealdade para resgatar,
 You have already assessed for the assessment criteria {}.,Você já avaliou os critérios de avaliação {}.,
 You have already selected items from {0} {1},Já selecionou itens de {0} {1},
 You have been invited to collaborate on the project: {0},Foi convidado para colaborar com o projeto: {0},
diff --git a/erpnext/translations/ro.csv b/erpnext/translations/ro.csv
index d3e2685..6c81419 100644
--- a/erpnext/translations/ro.csv
+++ b/erpnext/translations/ro.csv
@@ -3093,7 +3093,7 @@
 You cannot delete Project Type 'External',Nu puteți șterge tipul de proiect &quot;extern&quot;,
 You cannot edit root node.,Nu puteți edita nodul rădăcină.,
 You cannot restart a Subscription that is not cancelled.,Nu puteți reporni o abonament care nu este anulat.,
-You don't have enought Loyalty Points to redeem,Nu aveți puncte de loialitate pentru a răscumpăra,
+You don't have enough Loyalty Points to redeem,Nu aveți puncte de loialitate pentru a răscumpăra,
 You have already assessed for the assessment criteria {}.,Ați evaluat deja criteriile de evaluare {}.,
 You have already selected items from {0} {1},Ați selectat deja un produs de la {0} {1},
 You have been invited to collaborate on the project: {0},Ați fost invitat să colaboreze la proiect: {0},
diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv
index 405ce46..92442cd 100644
--- a/erpnext/translations/ru.csv
+++ b/erpnext/translations/ru.csv
@@ -3092,7 +3092,7 @@
 You cannot delete Project Type 'External',"Вы не можете удалить проект типа ""Внешний""",
 You cannot edit root node.,Вы не можете редактировать корневой узел.,
 You cannot restart a Subscription that is not cancelled.,"Вы не можете перезапустить подписку, которая не отменена.",
-You don't have enought Loyalty Points to redeem,У вас недостаточно очков лояльности для выкупа,
+You don't have enough Loyalty Points to redeem,У вас недостаточно очков лояльности для выкупа,
 You have already assessed for the assessment criteria {}.,Вы уже оценили критерии оценки {}.,
 You have already selected items from {0} {1},Вы уже выбрали продукты из {0} {1},
 You have been invited to collaborate on the project: {0},Вы были приглашены для совместной работы над проектом: {0},
diff --git a/erpnext/translations/rw.csv b/erpnext/translations/rw.csv
index ecad4f5..55b79fe 100644
--- a/erpnext/translations/rw.csv
+++ b/erpnext/translations/rw.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Ntushobora gusiba Ubwoko bwumushinga &#39;Hanze&#39;,
 You cannot edit root node.,Ntushobora guhindura imizi.,
 You cannot restart a Subscription that is not cancelled.,Ntushobora gutangira Kwiyandikisha bidahagaritswe.,
-You don't have enought Loyalty Points to redeem,Ntabwo ufite amanota ahagije yo gucungura,
+You don't have enough Loyalty Points to redeem,Ntabwo ufite amanota ahagije yo gucungura,
 You have already assessed for the assessment criteria {}.,Mumaze gusuzuma ibipimo ngenderwaho {}.,
 You have already selected items from {0} {1},Mumaze guhitamo ibintu kuva {0} {1},
 You have been invited to collaborate on the project: {0},Watumiwe gufatanya kumushinga: {0},
diff --git a/erpnext/translations/si.csv b/erpnext/translations/si.csv
index 568f892..b43af74 100644
--- a/erpnext/translations/si.csv
+++ b/erpnext/translations/si.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',ඔබට ව්යාපෘති වර්ගය &#39;බාහිර&#39;,
 You cannot edit root node.,ඔබට root node සංස්කරණය කළ නොහැක.,
 You cannot restart a Subscription that is not cancelled.,අවලංගු නොකළ දායකත්ව නැවත ආරම්භ කළ නොහැක.,
-You don't have enought Loyalty Points to redeem,ඔබ මුදා හැරීමට පක්ෂපාතීත්වයේ පොත්වලට ඔබ කැමති නැත,
+You don't have enough Loyalty Points to redeem,ඔබ මුදා හැරීමට පක්ෂපාතීත්වයේ පොත්වලට ඔබ කැමති නැත,
 You have already assessed for the assessment criteria {}.,තක්සේරු නිර්ණායක {} සඳහා ඔබ දැනටමත් තක්සේරු කර ඇත.,
 You have already selected items from {0} {1},ඔබ මේ වන විටත් {0} {1} සිට භාණ්ඩ තෝරාගෙන ඇති,
 You have been invited to collaborate on the project: {0},ඔබ මෙම ව්යාපෘතිය පිළිබඳව සහයෝගයෙන් කටයුතු කිරීමට ආරාධනා කර ඇත: {0},
diff --git a/erpnext/translations/sk.csv b/erpnext/translations/sk.csv
index a97f6c0..451b882 100644
--- a/erpnext/translations/sk.csv
+++ b/erpnext/translations/sk.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Nemôžete odstrániť typ projektu &quot;Externé&quot;,
 You cannot edit root node.,Nemôžete upraviť koreňový uzol.,
 You cannot restart a Subscription that is not cancelled.,"Predplatné, ktoré nie je zrušené, nemôžete reštartovať.",
-You don't have enought Loyalty Points to redeem,Nemáte dostatok vernostných bodov na vykúpenie,
+You don't have enough Loyalty Points to redeem,Nemáte dostatok vernostných bodov na vykúpenie,
 You have already assessed for the assessment criteria {}.,Vyhodnotili ste kritériá hodnotenia {}.,
 You have already selected items from {0} {1},Už ste vybrané položky z {0} {1},
 You have been invited to collaborate on the project: {0},Boli ste pozvaní k spolupráci na projekte: {0},
diff --git a/erpnext/translations/sl.csv b/erpnext/translations/sl.csv
index b653990..0cb7a6f 100644
--- a/erpnext/translations/sl.csv
+++ b/erpnext/translations/sl.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Ne morete izbrisati vrste projekta &quot;Zunanji&quot;,
 You cannot edit root node.,Rootnega vozlišča ne morete urejati.,
 You cannot restart a Subscription that is not cancelled.,"Naročnino, ki ni preklican, ne morete znova zagnati.",
-You don't have enought Loyalty Points to redeem,Za unovčevanje niste prejeli točk za zvestobo,
+You don't have enough Loyalty Points to redeem,Za unovčevanje niste prejeli točk za zvestobo,
 You have already assessed for the assessment criteria {}.,Ste že ocenili za ocenjevalnih meril {}.,
 You have already selected items from {0} {1},Ste že izbrane postavke iz {0} {1},
 You have been invited to collaborate on the project: {0},Ti so bili povabljeni k sodelovanju na projektu: {0},
diff --git a/erpnext/translations/sq.csv b/erpnext/translations/sq.csv
index 964c840..742dfcc 100644
--- a/erpnext/translations/sq.csv
+++ b/erpnext/translations/sq.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Ju nuk mund të fshini llojin e projektit &#39;Jashtë&#39;,
 You cannot edit root node.,Nuk mund të ndryshosh nyjen e rrënjës.,
 You cannot restart a Subscription that is not cancelled.,Nuk mund të rifilloni një Abonimi që nuk anulohet.,
-You don't have enought Loyalty Points to redeem,Ju nuk keni shumë pikat e Besnikërisë për të shpenguar,
+You don't have enough Loyalty Points to redeem,Ju nuk keni shumë pikat e Besnikërisë për të shpenguar,
 You have already assessed for the assessment criteria {}.,Ju kanë vlerësuar tashmë me kriteret e vlerësimit {}.,
 You have already selected items from {0} {1},Ju keni zgjedhur tashmë artikuj nga {0} {1},
 You have been invited to collaborate on the project: {0},Ju keni qenë të ftuar për të bashkëpunuar në këtë projekt: {0},
diff --git a/erpnext/translations/sr-SP.csv b/erpnext/translations/sr-SP.csv
index 25223db..bb28353 100644
--- a/erpnext/translations/sr-SP.csv
+++ b/erpnext/translations/sr-SP.csv
@@ -503,7 +503,7 @@
 Price List Rate,Cijena,
 Discount Amount,Vrijednost popusta,
 Sales Invoice Trends,Trendovi faktura prodaje,
-You don't have enought Loyalty Points to redeem,Немате довољно Бодова Лојалности.
+You don't have enough Loyalty Points to redeem,Немате довољно Бодова Лојалности.
 Tax Breakup,Porez po pozicijama,
 Task,Zadatak,
 Add / Edit Prices,Dodaj / Izmijeni cijene,
diff --git a/erpnext/translations/sr.csv b/erpnext/translations/sr.csv
index b8428d0..c5662ad 100644
--- a/erpnext/translations/sr.csv
+++ b/erpnext/translations/sr.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Не можете обрисати тип пројекта &#39;Спољни&#39;,
 You cannot edit root node.,Не можете уређивати роот чвор.,
 You cannot restart a Subscription that is not cancelled.,Не можете поново покренути претплату која није отказана.,
-You don't have enought Loyalty Points to redeem,Не искористите Лоиалти Поинтс за откуп,
+You don't have enough Loyalty Points to redeem,Не искористите Лоиалти Поинтс за откуп,
 You have already assessed for the assessment criteria {}.,Већ сте оцијенили за критеријуми за оцењивање {}.,
 You have already selected items from {0} {1},Који сте изабрали ставке из {0} {1},
 You have been invited to collaborate on the project: {0},Позвани сте да сарађују на пројекту: {0},
diff --git a/erpnext/translations/sr_sp.csv b/erpnext/translations/sr_sp.csv
index c121e6a..2383c6e 100644
--- a/erpnext/translations/sr_sp.csv
+++ b/erpnext/translations/sr_sp.csv
@@ -545,7 +545,7 @@
 You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings,Ne možete obrisati fiskalnu godinu {0}. Fiskalna  {0} godina je označena kao trenutna u globalnim podešavanjima.,
 You cannot delete Project Type 'External',"Не можете обрисати ""Спољни"" тип пројекта.",
 You cannot edit root node.,Не можете уређивати коренски чвор.,
-You don't have enought Loyalty Points to redeem,Немате довољно Бодова Лојалности.,
+You don't have enough Loyalty Points to redeem,Немате довољно Бодова Лојалности.,
 You have already assessed for the assessment criteria {}.,Већ сте оценили за критеријум оцењивања {}.,
 You have already selected items from {0} {1},Већ сте изабрали ставке из {0} {1},
 You have been invited to collaborate on the project: {0},Позвани сте да сарађујете на пројекту: {0},
diff --git a/erpnext/translations/sv.csv b/erpnext/translations/sv.csv
index f4ac1d5..abdbc6b 100644
--- a/erpnext/translations/sv.csv
+++ b/erpnext/translations/sv.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Du kan inte ta bort Project Type &#39;External&#39;,
 You cannot edit root node.,Du kan inte redigera rotknutpunkt.,
 You cannot restart a Subscription that is not cancelled.,Du kan inte starta om en prenumeration som inte avbryts.,
-You don't have enought Loyalty Points to redeem,Du har inte tillräckligt med lojalitetspoäng för att lösa in,
+You don't have enough Loyalty Points to redeem,Du har inte tillräckligt med lojalitetspoäng för att lösa in,
 You have already assessed for the assessment criteria {}.,Du har redan bedömt för bedömningskriterierna {}.,
 You have already selected items from {0} {1},Du har redan valt objekt från {0} {1},
 You have been invited to collaborate on the project: {0},Du har blivit inbjuden att samarbeta i projektet: {0},
diff --git a/erpnext/translations/sw.csv b/erpnext/translations/sw.csv
index 9f2504e..5f29c3f 100644
--- a/erpnext/translations/sw.csv
+++ b/erpnext/translations/sw.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Huwezi kufuta Aina ya Mradi &#39;Nje&#39;,
 You cannot edit root node.,Huwezi kubadilisha node ya mizizi.,
 You cannot restart a Subscription that is not cancelled.,Huwezi kuanzisha upya Usajili ambao haujahairiwa.,
-You don't have enought Loyalty Points to redeem,Huna ushawishi wa Pole ya Uaminifu ili ukomboe,
+You don't have enough Loyalty Points to redeem,Huna ushawishi wa Pole ya Uaminifu ili ukomboe,
 You have already assessed for the assessment criteria {}.,Tayari umehakikishia vigezo vya tathmini {}.,
 You have already selected items from {0} {1},Tayari umechagua vitu kutoka {0} {1},
 You have been invited to collaborate on the project: {0},Umealikwa kushirikiana kwenye mradi: {0},
diff --git a/erpnext/translations/ta.csv b/erpnext/translations/ta.csv
index cb8b83a..d36f47c 100644
--- a/erpnext/translations/ta.csv
+++ b/erpnext/translations/ta.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',நீங்கள் திட்டம் வகை &#39;வெளிப்புற&#39; நீக்க முடியாது,
 You cannot edit root node.,ரூட் முனையை நீங்கள் திருத்த முடியாது.,
 You cannot restart a Subscription that is not cancelled.,ரத்துசெய்யப்படாத சந்தாவை மறுதொடக்கம் செய்ய முடியாது.,
-You don't have enought Loyalty Points to redeem,நீங்கள் மீட்கும் விசுவாச புள்ளிகளைப் பெறுவீர்கள்,
+You don't have enough Loyalty Points to redeem,நீங்கள் மீட்கும் விசுவாச புள்ளிகளைப் பெறுவீர்கள்,
 You have already assessed for the assessment criteria {}.,ஏற்கனவே மதிப்பீட்டிற்குத் தகுதி மதிப்பீடு செய்யப்பட்டதன் {}.,
 You have already selected items from {0} {1},நீங்கள் ஏற்கனவே இருந்து பொருட்களை தேர்ந்தெடுத்த {0} {1},
 You have been invited to collaborate on the project: {0},நீங்கள் திட்டம் இணைந்து அழைக்கப்பட்டுள்ளனர்: {0},
diff --git a/erpnext/translations/te.csv b/erpnext/translations/te.csv
index a2f4960..83d9c8c 100644
--- a/erpnext/translations/te.csv
+++ b/erpnext/translations/te.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',మీరు ప్రాజెక్ట్ రకం &#39;బాహ్య&#39; తొలగించలేరు,
 You cannot edit root node.,మీరు రూట్ నోడ్ను సవరించలేరు.,
 You cannot restart a Subscription that is not cancelled.,మీరు రద్దు చేయని సభ్యత్వాన్ని పునఃప్రారంభించలేరు.,
-You don't have enought Loyalty Points to redeem,మీరు విమోచన చేయడానికి లాయల్టీ పాయింట్స్ను కలిగి ఉండరు,
+You don't have enough Loyalty Points to redeem,మీరు విమోచన చేయడానికి లాయల్టీ పాయింట్స్ను కలిగి ఉండరు,
 You have already assessed for the assessment criteria {}.,మీరు ఇప్పటికే అంచనా ప్రమాణం కోసం అంచనా {}.,
 You have already selected items from {0} {1},మీరు ఇప్పటికే ఎంపిక నుండి అంశాలను రోజులో {0} {1},
 You have been invited to collaborate on the project: {0},మీరు ప్రాజెక్ట్ సహకరించడానికి ఆహ్వానించబడ్డారు: {0},
diff --git a/erpnext/translations/th.csv b/erpnext/translations/th.csv
index af272fa..e492856 100644
--- a/erpnext/translations/th.csv
+++ b/erpnext/translations/th.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',คุณไม่สามารถลบประเภทโครงการ &#39;ภายนอก&#39;,
 You cannot edit root node.,คุณไม่สามารถแก้ไขโหนดรากได้,
 You cannot restart a Subscription that is not cancelled.,คุณไม่สามารถรีสตาร์ทการสมัครสมาชิกที่ไม่ได้ยกเลิกได้,
-You don't have enought Loyalty Points to redeem,คุณไม่มีจุดภักดีเพียงพอที่จะไถ่ถอน,
+You don't have enough Loyalty Points to redeem,คุณไม่มีจุดภักดีเพียงพอที่จะไถ่ถอน,
 You have already assessed for the assessment criteria {}.,คุณได้รับการประเมินเกณฑ์การประเมินแล้ว {},
 You have already selected items from {0} {1},คุณได้เลือกแล้วรายการจาก {0} {1},
 You have been invited to collaborate on the project: {0},คุณได้รับเชิญที่จะทำงานร่วมกันในโครงการ: {0},
diff --git a/erpnext/translations/tr.csv b/erpnext/translations/tr.csv
index 9c8fb3f..e0034c0 100644
--- a/erpnext/translations/tr.csv
+++ b/erpnext/translations/tr.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',&#39;Dış&#39; Proje Türünü silemezsiniz.,
 You cannot edit root node.,Kök düğümünü düzenleyemezsiniz.,
 You cannot restart a Subscription that is not cancelled.,İptal edilmeyen bir Aboneliği başlatamazsınız.,
-You don't have enought Loyalty Points to redeem,Kullanılması gereken sadakat puanlarına sahip olabilirsiniz,
+You don't have enough Loyalty Points to redeem,Kullanılması gereken sadakat puanlarına sahip olabilirsiniz,
 You have already assessed for the assessment criteria {}.,Zaten değerlendirme kriteri {} için değerlendirdiniz.,
 You have already selected items from {0} {1},Zaten öğelerinizi seçtiniz {0} {1},
 You have been invited to collaborate on the project: {0},{0} projesine davet edilmek için davet edildiniz,
diff --git a/erpnext/translations/uk.csv b/erpnext/translations/uk.csv
index 1b78f96..f4faedc 100644
--- a/erpnext/translations/uk.csv
+++ b/erpnext/translations/uk.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Ви не можете видалити тип проекту &quot;Зовнішній&quot;,
 You cannot edit root node.,Ви не можете редагувати кореневий вузол.,
 You cannot restart a Subscription that is not cancelled.,"Ви не можете перезапустити підписку, яку не скасовано.",
-You don't have enought Loyalty Points to redeem,"Ви не маєте впевнених точок лояльності, щоб викупити",
+You don't have enough Loyalty Points to redeem,"Ви не маєте впевнених точок лояльності, щоб викупити",
 You have already assessed for the assessment criteria {}.,Ви вже оцінили за критеріями оцінки {}.,
 You have already selected items from {0} {1},Ви вже вибрали елементи з {0} {1},
 You have been invited to collaborate on the project: {0},Ви були запрошені для спільної роботи над проектом: {0},
diff --git a/erpnext/translations/ur.csv b/erpnext/translations/ur.csv
index e32e594..e958e57 100644
--- a/erpnext/translations/ur.csv
+++ b/erpnext/translations/ur.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',آپ پراجیکٹ کی قسم کو خارج نہیں کرسکتے ہیں &#39;بیرونی&#39;,
 You cannot edit root node.,آپ جڑ نوڈ میں ترمیم نہیں کر سکتے ہیں.,
 You cannot restart a Subscription that is not cancelled.,آپ ایک سبسکرپشن کو دوبارہ شروع نہیں کرسکتے جو منسوخ نہیں ہوسکتا.,
-You don't have enought Loyalty Points to redeem,آپ کو بہت زیادہ وفادار پوائنٹس حاصل کرنے کے لئے نہیں ہے,
+You don't have enough Loyalty Points to redeem,آپ کو بہت زیادہ وفادار پوائنٹس حاصل کرنے کے لئے نہیں ہے,
 You have already assessed for the assessment criteria {}.,آپ نے پہلے ہی تشخیص کے معیار کے تعین کی ہے {}.,
 You have already selected items from {0} {1},آپ نے پہلے ہی سے اشیاء کو منتخب کیا ہے {0} {1},
 You have been invited to collaborate on the project: {0},آپ کو منصوبے پر تعاون کرنے کیلئے مدعو کیا گیا ہے: {0},
diff --git a/erpnext/translations/uz.csv b/erpnext/translations/uz.csv
index 3053559..6cdb23c 100644
--- a/erpnext/translations/uz.csv
+++ b/erpnext/translations/uz.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Siz &quot;Tashqi&quot; loyiha turini o&#39;chira olmaysiz,
 You cannot edit root node.,Ildiz tugunni tahrirlay olmaysiz.,
 You cannot restart a Subscription that is not cancelled.,Bekor qilinmagan obunani qayta boshlash mumkin emas.,
-You don't have enought Loyalty Points to redeem,Siz sotib olish uchun sodiqlik nuqtalari yo&#39;q,
+You don't have enough Loyalty Points to redeem,Siz sotib olish uchun sodiqlik nuqtalari yo&#39;q,
 You have already assessed for the assessment criteria {}.,Siz allaqachon baholash mezonlari uchun baholagansiz {}.,
 You have already selected items from {0} {1},{0} {1} dan tanlangan elementlarni tanladingiz,
 You have been invited to collaborate on the project: {0},Siz loyihada hamkorlik qilish uchun taklif qilingan: {0},
diff --git a/erpnext/translations/vi.csv b/erpnext/translations/vi.csv
index 5c74694..650ac28 100644
--- a/erpnext/translations/vi.csv
+++ b/erpnext/translations/vi.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',Bạn không thể xóa Loại dự án &#39;Bên ngoài&#39;,
 You cannot edit root node.,Bạn không thể chỉnh sửa nút gốc.,
 You cannot restart a Subscription that is not cancelled.,Bạn không thể khởi động lại Đăng ký không bị hủy.,
-You don't have enought Loyalty Points to redeem,Bạn không có Điểm trung thành đủ để đổi,
+You don't have enough Loyalty Points to redeem,Bạn không có Điểm trung thành đủ để đổi,
 You have already assessed for the assessment criteria {}.,Bạn đã đánh giá các tiêu chí đánh giá {}.,
 You have already selected items from {0} {1},Bạn đã chọn các mục từ {0} {1},
 You have been invited to collaborate on the project: {0},Bạn được lời mời cộng tác trong dự án: {0},
diff --git a/erpnext/translations/zh-TW.csv b/erpnext/translations/zh-TW.csv
index 25204f1..0209f44 100644
--- a/erpnext/translations/zh-TW.csv
+++ b/erpnext/translations/zh-TW.csv
@@ -1542,7 +1542,7 @@
 Course Intro,課程介紹
 MWS Auth Token,MWS Auth Token,
 Stock Entry {0} created,庫存輸入{0}創建
-You don't have enought Loyalty Points to redeem,您沒有獲得忠誠度積分兌換
+You don't have enough Loyalty Points to redeem,您沒有獲得忠誠度積分兌換
 Please set associated account in Tax Withholding Category {0} against Company {1},請在針對公司{1}的預扣稅分類{0}中設置關聯帳戶
 Row #{0}: Rejected Qty can not be entered in Purchase Return,行#{0}:駁回採購退貨數量不能進入
 Changing Customer Group for the selected Customer is not allowed.,不允許更改所選客戶的客戶組。
diff --git a/erpnext/translations/zh.csv b/erpnext/translations/zh.csv
index 9b2fbf0..d51bf6b 100644
--- a/erpnext/translations/zh.csv
+++ b/erpnext/translations/zh.csv
@@ -3094,7 +3094,7 @@
 You cannot delete Project Type 'External',您不能删除“外部”类型的项目,
 You cannot edit root node.,您不能编辑根节点。,
 You cannot restart a Subscription that is not cancelled.,您无法重新启动未取消的订阅。,
-You don't have enought Loyalty Points to redeem,您没有获得忠诚度积分兑换,
+You don't have enough Loyalty Points to redeem,您没有获得忠诚度积分兑换,
 You have already assessed for the assessment criteria {}.,您已经评估了评估标准{}。,
 You have already selected items from {0} {1},您已经选择从项目{0} {1},
 You have been invited to collaborate on the project: {0},您已被邀请在项目上进行合作:{0},
diff --git a/erpnext/translations/zh_tw.csv b/erpnext/translations/zh_tw.csv
index 54eb86a..75157f0 100644
--- a/erpnext/translations/zh_tw.csv
+++ b/erpnext/translations/zh_tw.csv
@@ -3127,7 +3127,7 @@
 You cannot delete Project Type 'External',您不能刪除項目類型“外部”,
 You cannot edit root node.,您不能編輯根節點。,
 You cannot restart a Subscription that is not cancelled.,您無法重新啟動未取消的訂閱。,
-You don't have enought Loyalty Points to redeem,您沒有獲得忠誠度積分兌換,
+You don't have enough Loyalty Points to redeem,您沒有獲得忠誠度積分兌換,
 You have already assessed for the assessment criteria {}.,您已經評估了評估標準{}。,
 You have already selected items from {0} {1},您已經選擇從項目{0} {1},
 You have been invited to collaborate on the project: {0},您已被邀請在項目上進行合作:{0},