Merge pull request #38250 from GursheenK/unset-discount-amount-based-on-coupon-code

fix: unset discount amount based on coupon code
diff --git a/README.md b/README.md
index 44bd729..710187a 100644
--- a/README.md
+++ b/README.md
@@ -73,8 +73,6 @@
 1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines)
 1. [Report Security Vulnerabilities](https://erpnext.com/security)
 1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
-1. [Translations](https://translate.erpnext.com)
-
 
 ## License
 
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json
index 2cd6c0f..4013bb0 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json
@@ -1,4 +1,6 @@
 {
+  "country_code": "hu", 
+  "name": "Hungary - Chart of Accounts for Microenterprises", 
   "tree": {
     "SZ\u00c1MLAOSZT\u00c1LY BEFEKTETETT ESZK\u00d6Z\u00d6K": {
       "account_number": 1,
@@ -1651,4 +1653,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 7e2f763..c2ddb39 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -424,7 +424,9 @@
 	vouchers = json.loads(vouchers)
 	transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
 	transaction.add_payment_entries(vouchers)
-	return frappe.get_doc("Bank Transaction", bank_transaction_name)
+	transaction.save()
+
+	return transaction
 
 
 @frappe.whitelist()
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
index b32022e..0328d51 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
@@ -13,6 +13,7 @@
   "status",
   "bank_account",
   "company",
+  "amended_from",
   "section_break_4",
   "deposit",
   "withdrawal",
@@ -25,10 +26,10 @@
   "transaction_id",
   "transaction_type",
   "section_break_14",
+  "column_break_oufv",
   "payment_entries",
   "section_break_18",
   "allocated_amount",
-  "amended_from",
   "column_break_17",
   "unallocated_amount",
   "party_section",
@@ -138,10 +139,12 @@
    "fieldtype": "Section Break"
   },
   {
+   "allow_on_submit": 1,
    "fieldname": "allocated_amount",
    "fieldtype": "Currency",
    "label": "Allocated Amount",
-   "options": "currency"
+   "options": "currency",
+   "read_only": 1
   },
   {
    "fieldname": "amended_from",
@@ -157,10 +160,12 @@
    "fieldtype": "Column Break"
   },
   {
+   "allow_on_submit": 1,
    "fieldname": "unallocated_amount",
    "fieldtype": "Currency",
    "label": "Unallocated Amount",
-   "options": "currency"
+   "options": "currency",
+   "read_only": 1
   },
   {
    "fieldname": "party_section",
@@ -225,11 +230,15 @@
    "fieldname": "bank_party_account_number",
    "fieldtype": "Data",
    "label": "Party Account No. (Bank Statement)"
+  },
+  {
+   "fieldname": "column_break_oufv",
+   "fieldtype": "Column Break"
   }
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2023-06-06 13:58:12.821411",
+ "modified": "2023-11-18 18:32:47.203694",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Bank Transaction",
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 4649d23..51c823a 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -2,78 +2,73 @@
 # For license information, please see license.txt
 
 import frappe
+from frappe import _
 from frappe.utils import flt
 
 from erpnext.controllers.status_updater import StatusUpdater
 
 
 class BankTransaction(StatusUpdater):
-	def after_insert(self):
-		self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit))
+	def before_validate(self):
+		self.update_allocated_amount()
 
-	def on_submit(self):
-		self.clear_linked_payment_entries()
+	def validate(self):
+		self.validate_duplicate_references()
+
+	def validate_duplicate_references(self):
+		"""Make sure the same voucher is not allocated twice within the same Bank Transaction"""
+		if not self.payment_entries:
+			return
+
+		pe = []
+		for row in self.payment_entries:
+			reference = (row.payment_document, row.payment_entry)
+			if reference in pe:
+				frappe.throw(
+					_("{0} {1} is allocated twice in this Bank Transaction").format(
+						row.payment_document, row.payment_entry
+					)
+				)
+			pe.append(reference)
+
+	def update_allocated_amount(self):
+		self.allocated_amount = (
+			sum(p.allocated_amount for p in self.payment_entries) if self.payment_entries else 0.0
+		)
+		self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.allocated_amount
+
+	def before_submit(self):
+		self.allocate_payment_entries()
 		self.set_status()
 
 		if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
 			self.auto_set_party()
 
-	_saving_flag = False
-
-	# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
-	def on_update_after_submit(self):
-		"Run on save(). Avoid recursion caused by multiple saves"
-		if not self._saving_flag:
-			self._saving_flag = True
-			self.clear_linked_payment_entries()
-			self.update_allocations()
-			self._saving_flag = False
+	def before_update_after_submit(self):
+		self.validate_duplicate_references()
+		self.allocate_payment_entries()
+		self.update_allocated_amount()
 
 	def on_cancel(self):
-		self.clear_linked_payment_entries(for_cancel=True)
-		self.set_status(update=True)
+		for payment_entry in self.payment_entries:
+			self.clear_linked_payment_entry(payment_entry, for_cancel=True)
 
-	def update_allocations(self):
-		"The doctype does not allow modifications after submission, so write to the db direct"
-		if self.payment_entries:
-			allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
-		else:
-			allocated_amount = 0.0
-
-		amount = abs(flt(self.withdrawal) - flt(self.deposit))
-		self.db_set("allocated_amount", flt(allocated_amount))
-		self.db_set("unallocated_amount", amount - flt(allocated_amount))
-		self.reload()
 		self.set_status(update=True)
 
 	def add_payment_entries(self, vouchers):
 		"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
 		if 0.0 >= self.unallocated_amount:
-			frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
+			frappe.throw(_("Bank Transaction {0} is already fully reconciled").format(self.name))
 
-		added = False
 		for voucher in vouchers:
-			# Can't add same voucher twice
-			found = False
-			for pe in self.payment_entries:
-				if (
-					pe.payment_document == voucher["payment_doctype"]
-					and pe.payment_entry == voucher["payment_name"]
-				):
-					found = True
-
-			if not found:
-				pe = {
+			self.append(
+				"payment_entries",
+				{
 					"payment_document": voucher["payment_doctype"],
 					"payment_entry": voucher["payment_name"],
 					"allocated_amount": 0.0,  # Temporary
-				}
-				child = self.append("payment_entries", pe)
-				added = True
-
-		# runs on_update_after_submit
-		if added:
-			self.save()
+				},
+			)
 
 	def allocate_payment_entries(self):
 		"""Refactored from bank reconciliation tool.
@@ -90,6 +85,7 @@
 		- clear means: set the latest transaction date as clearance date
 		"""
 		remaining_amount = self.unallocated_amount
+		to_remove = []
 		for payment_entry in self.payment_entries:
 			if payment_entry.allocated_amount == 0.0:
 				unallocated_amount, should_clear, latest_transaction = get_clearance_details(
@@ -99,49 +95,39 @@
 				if 0.0 == unallocated_amount:
 					if should_clear:
 						latest_transaction.clear_linked_payment_entry(payment_entry)
-					self.db_delete_payment_entry(payment_entry)
+					to_remove.append(payment_entry)
 
 				elif remaining_amount <= 0.0:
-					self.db_delete_payment_entry(payment_entry)
+					to_remove.append(payment_entry)
 
-				elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
-					payment_entry.db_set("allocated_amount", unallocated_amount)
+				elif 0.0 < unallocated_amount <= remaining_amount:
+					payment_entry.allocated_amount = unallocated_amount
 					remaining_amount -= unallocated_amount
 					if should_clear:
 						latest_transaction.clear_linked_payment_entry(payment_entry)
 
-				elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
-					payment_entry.db_set("allocated_amount", remaining_amount)
+				elif 0.0 < unallocated_amount:
+					payment_entry.allocated_amount = remaining_amount
 					remaining_amount = 0.0
 
 				elif 0.0 > unallocated_amount:
-					self.db_delete_payment_entry(payment_entry)
-					frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
+					frappe.throw(_("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
 
-		self.reload()
-
-	def db_delete_payment_entry(self, payment_entry):
-		frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
+		for payment_entry in to_remove:
+			self.remove(to_remove)
 
 	@frappe.whitelist()
 	def remove_payment_entries(self):
 		for payment_entry in self.payment_entries:
 			self.remove_payment_entry(payment_entry)
-		# runs on_update_after_submit
-		self.save()
+
+		self.save()  # runs before_update_after_submit
 
 	def remove_payment_entry(self, payment_entry):
 		"Clear payment entry and clearance"
 		self.clear_linked_payment_entry(payment_entry, for_cancel=True)
 		self.remove(payment_entry)
 
-	def clear_linked_payment_entries(self, for_cancel=False):
-		if for_cancel:
-			for payment_entry in self.payment_entries:
-				self.clear_linked_payment_entry(payment_entry, for_cancel)
-		else:
-			self.allocate_payment_entries()
-
 	def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
 		clearance_date = None if for_cancel else self.date
 		set_voucher_clearance(
@@ -162,11 +148,10 @@
 			deposit=self.deposit,
 		).match()
 
-		if result:
-			party_type, party = result
-			frappe.db.set_value(
-				"Bank Transaction", self.name, field={"party_type": party_type, "party": party}
-			)
+		if not result:
+			return
+
+		self.party_type, self.party = result
 
 
 @frappe.whitelist()
@@ -198,9 +183,7 @@
 		if gle["gl_account"] == gl_bank_account:
 			if gle["amount"] <= 0.0:
 				frappe.throw(
-					frappe._("Voucher {0} value is broken: {1}").format(
-						payment_entry.payment_entry, gle["amount"]
-					)
+					_("Voucher {0} value is broken: {1}").format(payment_entry.payment_entry, gle["amount"])
 				)
 
 			unmatched_gles -= 1
@@ -221,7 +204,7 @@
 
 def get_related_bank_gl_entries(doctype, docname):
 	# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
-	result = frappe.db.sql(
+	return frappe.db.sql(
 		"""
 		SELECT
 			ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
@@ -239,7 +222,6 @@
 		dict(doctype=doctype, docname=docname),
 		as_dict=True,
 	)
-	return result
 
 
 def get_total_allocated_amount(doctype, docname):
@@ -365,6 +347,7 @@
 		if clearance_date:
 			vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
 			bt.add_payment_entries(vouchers)
+			bt.save()
 		else:
 			for pe in bt.payment_entries:
 				if pe.payment_document == self.doctype and pe.payment_entry == self.name:
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index 5a1c139..1e64eee 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -113,7 +113,7 @@
 			if as_dict:
 				data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
 			else:
-				if not row[1]:
+				if not row[1] and len(row) > 1:
 					row[1] = row[0]
 					row[3] = row[2]
 				data.append(row)
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 22b6880..9684a0d 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -51,7 +51,7 @@
 				}, __('Make'));
 		}
 
-		erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
+		erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
 	},
 	before_save: function(frm) {
 		if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 2eb54a5..906760e 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -548,8 +548,16 @@
  "icon": "fa fa-file-text",
  "idx": 176,
  "is_submittable": 1,
- "links": [],
- "modified": "2023-08-10 14:32:22.366895",
+ "links": [
+  {
+   "is_child_table": 1,
+   "link_doctype": "Bank Transaction Payments",
+   "link_fieldname": "payment_entry",
+   "parent_doctype": "Bank Transaction",
+   "table_fieldname": "payment_entries"
+  }
+ ],
+ "modified": "2023-11-23 12:11:04.128015",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Journal Entry",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 85ef6f7..0ad20c3 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -508,7 +508,7 @@
 							).format(d.reference_name, d.account)
 						)
 				else:
-					dr_or_cr = "debit" if d.credit > 0 else "credit"
+					dr_or_cr = "debit" if flt(d.credit) > 0 else "credit"
 					valid = False
 					for jvd in against_entries:
 						if flt(jvd[dr_or_cr]) > 0:
@@ -868,7 +868,7 @@
 					party_account_currency = d.account_currency
 
 			elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
-				bank_amount += d.debit_in_account_currency or d.credit_in_account_currency
+				bank_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency)
 				bank_account_currency = d.account_currency
 
 		if party_type and pay_to_recd_from:
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 3ba8cea..3132fe9 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,8 @@
    "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\nPayment Entry"
+   "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",
+   "search_index": 1
   },
   {
    "fieldname": "reference_name",
@@ -211,7 +212,8 @@
    "in_list_view": 1,
    "label": "Reference Name",
    "no_copy": 1,
-   "options": "reference_type"
+   "options": "reference_type",
+   "search_index": 1
   },
   {
    "depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
@@ -278,13 +280,14 @@
    "fieldtype": "Data",
    "hidden": 1,
    "label": "Reference Detail No",
-   "no_copy": 1
+   "no_copy": 1,
+   "search_index": 1
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-06-16 14:11:13.507807",
+ "modified": "2023-11-23 11:44:25.841187",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Journal Entry Account",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 9a6f8ec..2611240 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', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries'];
+		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries'];
 
 		if(frm.doc.__islocal) {
 			if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
@@ -160,7 +160,7 @@
 			}, __('Actions'));
 
 		}
-		erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
+		erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
 	},
 
 	validate_company: (frm) => {
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 4d50a35..aa18156 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -750,8 +750,16 @@
  ],
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
- "links": [],
- "modified": "2023-11-08 21:51:03.482709",
+ "links": [
+  {
+   "is_child_table": 1,
+   "link_doctype": "Bank Transaction Payments",
+   "link_fieldname": "payment_entry",
+   "parent_doctype": "Bank Transaction",
+   "table_fieldname": "payment_entries"
+  }
+ ],
+ "modified": "2023-11-23 12:07:20.887885",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Payment Entry",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index ef304bc..0344e3d 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -148,7 +148,7 @@
 			"Repost Payment Ledger Items",
 			"Repost Accounting Ledger",
 			"Repost Accounting Ledger Items",
-			"Unreconcile Payments",
+			"Unreconcile Payment",
 			"Unreconcile Payment Entries",
 		)
 		super(PaymentEntry, self).on_cancel()
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
index b88791d..ccb9e64 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
@@ -212,9 +212,10 @@
  ],
  "hide_toolbar": 1,
  "icon": "icon-resize-horizontal",
+ "is_virtual": 1,
  "issingle": 1,
  "links": [],
- "modified": "2023-08-15 05:35:50.109290",
+ "modified": "2023-11-17 17:33:55.701726",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Payment Reconciliation",
@@ -239,6 +240,5 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
- "states": [],
- "track_changes": 1
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 43167be..6673e8d 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -29,6 +29,58 @@
 		self.accounting_dimension_filter_conditions = []
 		self.ple_posting_date_filter = []
 
+	def load_from_db(self):
+		# 'modified' attribute is required for `run_doc_method` to work properly.
+		doc_dict = frappe._dict(
+			{
+				"modified": None,
+				"company": None,
+				"party": None,
+				"party_type": None,
+				"receivable_payable_account": None,
+				"default_advance_account": None,
+				"from_invoice_date": None,
+				"to_invoice_date": None,
+				"invoice_limit": 50,
+				"from_payment_date": None,
+				"to_payment_date": None,
+				"payment_limit": 50,
+				"minimum_invoice_amount": None,
+				"minimum_payment_amount": None,
+				"maximum_invoice_amount": None,
+				"maximum_payment_amount": None,
+				"bank_cash_account": None,
+				"cost_center": None,
+				"payment_name": None,
+				"invoice_name": None,
+			}
+		)
+		super(Document, self).__init__(doc_dict)
+
+	def save(self):
+		return
+
+	@staticmethod
+	def get_list(args):
+		pass
+
+	@staticmethod
+	def get_count(args):
+		pass
+
+	@staticmethod
+	def get_stats(args):
+		pass
+
+	def db_insert(self, *args, **kwargs):
+		pass
+
+	def db_update(self, *args, **kwargs):
+		pass
+
+	def delete(self):
+		pass
+
 	@frappe.whitelist()
 	def get_unreconciled_entries(self):
 		self.get_nonreconciled_payment_entries()
diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json
index 5b8556e..491c678 100644
--- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json
@@ -159,9 +159,10 @@
    "label": "Difference Posting Date"
   }
  ],
+ "is_virtual": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-10-23 10:44:56.066303",
+ "modified": "2023-11-17 17:33:38.612615",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Payment Reconciliation Allocation",
diff --git a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json
index c4dbd7e..7c9d49e 100644
--- a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json
+++ b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json
@@ -71,9 +71,10 @@
    "label": "Exchange Rate"
   }
  ],
+ "is_virtual": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-11-08 18:18:02.502149",
+ "modified": "2023-11-17 17:33:45.455166",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Payment Reconciliation Invoice",
diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json
index 17f3900..d199236 100644
--- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json
+++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json
@@ -107,9 +107,10 @@
    "options": "Cost Center"
   }
  ],
+ "is_virtual": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-09-03 07:43:29.965353",
+ "modified": "2023-11-17 17:33:34.818530",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Payment Reconciliation Payment",
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index e36e97b..9091a77 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -556,7 +556,7 @@
 		return bin_qty - pos_sales_qty, is_stock_item
 	else:
 		is_stock_item = True
-		if frappe.db.exists("Product Bundle", item_code):
+		if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
 			return get_bundle_availability(item_code, warehouse), is_stock_item
 		else:
 			is_stock_item = False
diff --git a/erpnext/accounts/doctype/process_subscription/process_subscription.py b/erpnext/accounts/doctype/process_subscription/process_subscription.py
index 99269d6..0aa9970 100644
--- a/erpnext/accounts/doctype/process_subscription/process_subscription.py
+++ b/erpnext/accounts/doctype/process_subscription/process_subscription.py
@@ -17,11 +17,10 @@
 
 
 def create_subscription_process(
-	subscription: str | None, posting_date: Union[str, datetime.date] | None
+	subscription: str | None = None, posting_date: Union[str, datetime.date] | None = None
 ):
 	"""Create a new Process Subscription document"""
 	doc = frappe.new_doc("Process Subscription")
 	doc.subscription = subscription
 	doc.posting_date = getdate(posting_date)
-	doc.insert(ignore_permissions=True)
 	doc.submit()
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 2eaa337..4b0df12 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -180,7 +180,7 @@
 		}
 
 		this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
-		erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
+		erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
 	}
 
 	unblock_invoice() {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index e1f0f19..c6ae937 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -13,6 +13,7 @@
 from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
 from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
 	validate_docs_for_deferred_accounting,
+	validate_docs_for_voucher_types,
 )
 from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
 	check_if_return_invoice_linked_with_payment_entry,
@@ -491,6 +492,7 @@
 	def validate_for_repost(self):
 		self.validate_write_off_account()
 		self.validate_expense_account()
+		validate_docs_for_voucher_types(["Purchase Invoice"])
 		validate_docs_for_deferred_accounting([], [self.name])
 
 	def on_submit(self):
@@ -525,7 +527,11 @@
 		if self.update_stock == 1:
 			self.repost_future_sle_and_gle()
 
-		self.update_project()
+		if (
+			frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction"
+		):
+			self.update_project()
+
 		update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
 		self.update_advance_tax_references()
 
@@ -1260,7 +1266,10 @@
 		if self.update_stock == 1:
 			self.repost_future_sle_and_gle()
 
-		self.update_project()
+		if (
+			frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction"
+		):
+			self.update_project()
 		self.db_set("status", "Cancelled")
 
 		unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
@@ -1279,13 +1288,21 @@
 		self.update_advance_tax_references(cancel=1)
 
 	def update_project(self):
-		project_list = []
+		projects = frappe._dict()
 		for d in self.items:
-			if d.project and d.project not in project_list:
-				project = frappe.get_doc("Project", d.project)
-				project.update_purchase_costing()
-				project.db_update()
-				project_list.append(d.project)
+			if d.project:
+				if self.docstatus == 1:
+					projects[d.project] = projects.get(d.project, 0) + d.base_net_amount
+				elif self.docstatus == 2:
+					projects[d.project] = projects.get(d.project, 0) - d.base_net_amount
+
+		pj = frappe.qb.DocType("Project")
+		for proj, value in projects.items():
+			res = (
+				frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run()
+			)
+			current_purchase_cost = res and res[0][0] or 0
+			frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value)
 
 	def validate_supplier_invoice(self):
 		if self.bill_date:
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index bcedb7c..71796c9 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -498,6 +498,7 @@
    "fieldtype": "Column Break"
   },
   {
+   "allow_on_submit": 1,
    "fieldname": "project",
    "fieldtype": "Link",
    "label": "Project",
@@ -505,6 +506,7 @@
    "print_hide": 1
   },
   {
+   "allow_on_submit": 1,
    "default": ":Company",
    "depends_on": "eval:!doc.is_fixed_asset",
    "fieldname": "cost_center",
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
index 69cfe9f..1d72a46 100644
--- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
@@ -10,12 +10,7 @@
 class RepostAccountingLedger(Document):
 	def __init__(self, *args, **kwargs):
 		super(RepostAccountingLedger, self).__init__(*args, **kwargs)
-		self._allowed_types = [
-			x.document_type
-			for x in frappe.db.get_all(
-				"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
-			)
-		]
+		self._allowed_types = get_allowed_types_from_settings()
 
 	def validate(self):
 		self.validate_vouchers()
@@ -56,15 +51,7 @@
 
 	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))),
-					)
-				)
+			validate_docs_for_voucher_types([x.voucher_type for x in self.vouchers])
 
 	def get_existing_ledger_entries(self):
 		vouchers = [x.voucher_no for x in self.vouchers]
@@ -168,6 +155,15 @@
 				frappe.db.commit()
 
 
+def get_allowed_types_from_settings():
+	return [
+		x.document_type
+		for x in frappe.db.get_all(
+			"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
+		)
+	]
+
+
 def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
 	docs_with_deferred_revenue = frappe.db.get_all(
 		"Sales Invoice Item",
@@ -191,6 +187,25 @@
 		)
 
 
+def validate_docs_for_voucher_types(doc_voucher_types):
+	allowed_types = get_allowed_types_from_settings()
+	# Validate voucher types
+	voucher_types = set(doc_voucher_types)
+	if disallowed_types := voucher_types.difference(allowed_types):
+		message = "are" if len(disallowed_types) > 1 else "is"
+		frappe.throw(
+			_("{0} {1} not allowed to be reposted. Modify {2} to enable reposting.").format(
+				frappe.bold(comma_and(list(disallowed_types))),
+				message,
+				frappe.bold(
+					frappe.utils.get_link_to_form(
+						"Repost Accounting Ledger Settings", "Repost Accounting Ledger Settings"
+					)
+				),
+			)
+		)
+
+
 @frappe.whitelist()
 @frappe.validate_and_sanitize_search_inputs
 def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index b1a7b10..6763e44 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", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"];
+							  'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
 
 		if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
 			// show debit_to in print format
@@ -184,7 +184,7 @@
 			}
 		}
 
-		erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
+		erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
 	}
 
 	make_maintenance_schedule() {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index d167783..f209487 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1615,7 +1615,8 @@
    "hide_seconds": 1,
    "label": "Inter Company Invoice Reference",
    "options": "Purchase Invoice",
-   "read_only": 1
+   "read_only": 1,
+   "search_index": 1
   },
   {
    "fieldname": "customer_group",
@@ -2173,7 +2174,7 @@
    "link_fieldname": "consolidated_invoice"
   }
  ],
- "modified": "2023-11-20 11:51:43.555197",
+ "modified": "2023-11-23 16:56:29.679499",
  "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 fa95ccd..85cb367 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -17,6 +17,7 @@
 )
 from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
 	validate_docs_for_deferred_accounting,
+	validate_docs_for_voucher_types,
 )
 from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
 	get_party_tax_withholding_details,
@@ -172,6 +173,7 @@
 		self.validate_write_off_account()
 		self.validate_account_for_change_amount()
 		self.validate_income_account()
+		validate_docs_for_voucher_types(["Sales Invoice"])
 		validate_docs_for_deferred_accounting([self.name], [])
 
 	def validate_fixed_asset(self):
@@ -395,7 +397,7 @@
 			"Repost Payment Ledger Items",
 			"Repost Accounting Ledger",
 			"Repost Accounting Ledger Items",
-			"Unreconcile Payments",
+			"Unreconcile Payment",
 			"Unreconcile Payment Entries",
 			"Payment Ledger Entry",
 			"Serial and Batch Bundle",
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 3cf7d28..a3d8c23 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -676,7 +676,7 @@
 
 
 def process_all(
-	subscription: str | None, posting_date: Optional["DateTimeLikeObject"] = None
+	subscription: str | None = None, posting_date: Optional["DateTimeLikeObject"] = None
 ) -> None:
 	"""
 	Task to updates the status of all `Subscription` apart from those that are cancelled
diff --git a/erpnext/accounts/doctype/unreconcile_payments/__init__.py b/erpnext/accounts/doctype/unreconcile_payment/__init__.py
similarity index 100%
rename from erpnext/accounts/doctype/unreconcile_payments/__init__.py
rename to erpnext/accounts/doctype/unreconcile_payment/__init__.py
diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py
similarity index 96%
rename from erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py
rename to erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py
index 78e04bf..f404d99 100644
--- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py
+++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py
@@ -10,7 +10,7 @@
 from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
 
 
-class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
+class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
 	def setUp(self):
 		self.create_company()
 		self.create_customer()
@@ -73,7 +73,7 @@
 
 		unreconcile = frappe.get_doc(
 			{
-				"doctype": "Unreconcile Payments",
+				"doctype": "Unreconcile Payment",
 				"company": self.company,
 				"voucher_type": pe.doctype,
 				"voucher_no": pe.name,
@@ -138,7 +138,7 @@
 
 		unreconcile = frappe.get_doc(
 			{
-				"doctype": "Unreconcile Payments",
+				"doctype": "Unreconcile Payment",
 				"company": self.company,
 				"voucher_type": pe2.doctype,
 				"voucher_no": pe2.name,
@@ -196,7 +196,7 @@
 
 		unreconcile = frappe.get_doc(
 			{
-				"doctype": "Unreconcile Payments",
+				"doctype": "Unreconcile Payment",
 				"company": self.company,
 				"voucher_type": pe.doctype,
 				"voucher_no": pe.name,
@@ -281,7 +281,7 @@
 
 		unreconcile = frappe.get_doc(
 			{
-				"doctype": "Unreconcile Payments",
+				"doctype": "Unreconcile Payment",
 				"company": self.company,
 				"voucher_type": pe2.doctype,
 				"voucher_no": pe2.name,
diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js
similarity index 93%
rename from erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js
rename to erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js
index c522567..70cefb1 100644
--- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js
+++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.js
@@ -1,7 +1,7 @@
 // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
 // For license information, please see license.txt
 
-frappe.ui.form.on("Unreconcile Payments", {
+frappe.ui.form.on("Unreconcile Payment", {
 	refresh(frm) {
 		frm.set_query("voucher_type", function() {
 			return {
diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json
similarity index 95%
rename from erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json
rename to erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json
index f29e61b..f906dc6 100644
--- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json
+++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json
@@ -21,7 +21,7 @@
    "fieldtype": "Link",
    "label": "Amended From",
    "no_copy": 1,
-   "options": "Unreconcile Payments",
+   "options": "Unreconcile Payment",
    "print_hide": 1,
    "read_only": 1
   },
@@ -61,7 +61,7 @@
  "modified": "2023-08-28 17:42:50.261377",
  "modified_by": "Administrator",
  "module": "Accounts",
- "name": "Unreconcile Payments",
+ "name": "Unreconcile Payment",
  "naming_rule": "Expression",
  "owner": "Administrator",
  "permissions": [
@@ -90,4 +90,4 @@
  "sort_order": "DESC",
  "states": [],
  "track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py
similarity index 97%
rename from erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py
rename to erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py
index 4f9fb50..77906a7 100644
--- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py
+++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py
@@ -15,7 +15,7 @@
 )
 
 
-class UnreconcilePayments(Document):
+class UnreconcilePayment(Document):
 	def validate(self):
 		self.supported_types = ["Payment Entry", "Journal Entry"]
 		if not self.voucher_type in self.supported_types:
@@ -142,7 +142,7 @@
 		selections = frappe.json.loads(selections)
 		# assuming each row is a unique voucher
 		for row in selections:
-			unrecon = frappe.new_doc("Unreconcile Payments")
+			unrecon = frappe.new_doc("Unreconcile Payment")
 			unrecon.company = row.get("company")
 			unrecon.voucher_type = row.get("voucher_type")
 			unrecon.voucher_no = row.get("voucher_no")
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 0e62ad6..7948e5f 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -285,8 +285,8 @@
 
 			must_consider = False
 			if self.filters.get("for_revaluation_journals"):
-				if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) or (
-					(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
+				if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or (
+					(abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision)
 				):
 					must_consider = True
 			else:
diff --git a/erpnext/accounts/report/sales_register/sales_register.js b/erpnext/accounts/report/sales_register/sales_register.js
index 1a41172..4578ac3 100644
--- a/erpnext/accounts/report/sales_register/sales_register.js
+++ b/erpnext/accounts/report/sales_register/sales_register.js
@@ -23,6 +23,12 @@
 			"options": "Customer"
 		},
 		{
+			"fieldname":"customer_group",
+			"label": __("Customer Group"),
+			"fieldtype": "Link",
+			"options": "Customer Group"
+		},
+		{
 			"fieldname":"company",
 			"label": __("Company"),
 			"fieldtype": "Link",
diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py
index 0ba7186..ec6dd72 100644
--- a/erpnext/accounts/report/sales_register/sales_register.py
+++ b/erpnext/accounts/report/sales_register/sales_register.py
@@ -449,6 +449,9 @@
 	if filters.get("customer"):
 		query = query.where(si.customer == filters.customer)
 
+	if filters.get("customer_group"):
+		query = query.where(si.customer_group == filters.customer_group)
+
 	query = get_conditions(filters, query, "Sales Invoice")
 	query = apply_common_conditions(
 		filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item"
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
index 82f97f1..2b5566f 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
@@ -1,7 +1,7 @@
 import frappe
 from frappe import _
 
-from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import (
+from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import (
 	get_result,
 	get_tds_docs,
 )
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 7d91309..9d32a03 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -183,6 +183,7 @@
 	cost_center=None,
 	ignore_account_permission=False,
 	account_type=None,
+	start_date=None,
 ):
 	if not account and frappe.form_dict.get("account"):
 		account = frappe.form_dict.get("account")
@@ -196,6 +197,8 @@
 		cost_center = frappe.form_dict.get("cost_center")
 
 	cond = ["is_cancelled=0"]
+	if start_date:
+		cond.append("posting_date >= %s" % frappe.db.escape(cstr(start_date)))
 	if date:
 		cond.append("posting_date <= %s" % frappe.db.escape(cstr(date)))
 	else:
@@ -1833,6 +1836,28 @@
 					Table("outstanding").amount_in_account_currency >= self.max_outstanding
 				)
 
+		if self.limit and self.get_invoices:
+			outstanding_vouchers = (
+				qb.from_(ple)
+				.select(
+					ple.against_voucher_no.as_("voucher_no"),
+					Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
+				)
+				.where(ple.delinked == 0)
+				.where(Criterion.all(filter_on_against_voucher_no))
+				.where(Criterion.all(self.common_filter))
+				.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
+				.orderby(ple.posting_date, ple.voucher_no)
+				.having(qb.Field("amount_in_account_currency") > 0)
+				.limit(self.limit)
+				.run()
+			)
+			if outstanding_vouchers:
+				filter_on_voucher_no.append(ple.voucher_no.isin([x[0] for x in outstanding_vouchers]))
+				filter_on_against_voucher_no.append(
+					ple.against_voucher_no.isin([x[0] for x in outstanding_vouchers])
+				)
+
 		# build query for voucher amount
 		query_voucher_amount = (
 			qb.from_(ple)
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index d378fbd..58fd6d4 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -214,30 +214,43 @@
 		})
 	},
 
-	render_depreciation_schedule_view: function(frm, depr_schedule) {
+	render_depreciation_schedule_view: function(frm, asset_depr_schedule_doc) {
 		let wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty();
 
 		let data = [];
 
-		depr_schedule.forEach((sch) => {
+		asset_depr_schedule_doc.depreciation_schedule.forEach((sch) => {
 			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'] || ''
+				sch['journal_entry'] || '',
 			];
+
+			if (asset_depr_schedule_doc.shift_based) {
+				row.push(sch['shift']);
+			}
+
 			data.push(row);
 		});
 
+		let 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},
+		];
+
+		if (asset_depr_schedule_doc.shift_based) {
+			columns.push({name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 245});
+			columns.push({name: __("Shift"), editable: false, resizable: false, width: 59});
+		} else {
+			columns.push({name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 304});
+		}
+
 		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: 304}
-			],
+			columns: columns,
 			data: data,
 			layout: "fluid",
 			serialNoColumn: false,
@@ -272,8 +285,8 @@
 				asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount')));
 			}
 
-			let depr_schedule = (await frappe.call(
-				"erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule",
+			let asset_depr_schedule_doc = (await frappe.call(
+				"erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_asset_depr_schedule_doc",
 				{
 					asset_name: frm.doc.name,
 					status: "Active",
@@ -281,7 +294,7 @@
 				}
 			)).message;
 
-			$.each(depr_schedule || [], function(i, v) {
+			$.each(asset_depr_schedule_doc.depreciation_schedule || [], function(i, v) {
 				x_intervals.push(frappe.format(v.schedule_date, { fieldtype: 'Date' }));
 				var asset_value = flt(frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, precision('gross_purchase_amount'));
 				if(v.journal_entry) {
@@ -296,7 +309,7 @@
 			});
 
 			frm.toggle_display(["depreciation_schedule_view"], 1);
-			frm.events.render_depreciation_schedule_view(frm, depr_schedule);
+			frm.events.render_depreciation_schedule_view(frm, asset_depr_schedule_doc);
 		} else {
 			if(frm.doc.opening_accumulated_depreciation) {
 				x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' }));
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 7b7953b..d1f03f2 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -825,6 +825,7 @@
 				"total_number_of_depreciations": d.total_number_of_depreciations,
 				"frequency_of_depreciation": d.frequency_of_depreciation,
 				"daily_prorata_based": d.daily_prorata_based,
+				"shift_based": d.shift_based,
 				"salvage_value_percentage": d.salvage_value_percentage,
 				"expected_value_after_useful_life": flt(gross_purchase_amount)
 				* flt(d.salvage_value_percentage / 100),
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index 84a428c..66930c0 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -509,6 +509,9 @@
 
 
 def depreciate_asset(asset_doc, date, notes):
+	if not asset_doc.calculate_depreciation:
+		return
+
 	asset_doc.flags.ignore_validate_update_after_submit = True
 
 	make_new_active_asset_depr_schedules_and_cancel_current_ones(
@@ -521,6 +524,9 @@
 
 
 def reset_depreciation_schedule(asset_doc, date, notes):
+	if not asset_doc.calculate_depreciation:
+		return
+
 	asset_doc.flags.ignore_validate_update_after_submit = True
 
 	make_new_active_asset_depr_schedules_and_cancel_current_ones(
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 9e3ec6f..dc80aa5 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -149,12 +149,7 @@
 			("Creditors - _TC", 0.0, 100000.0),
 		)
 
-		gle = frappe.db.sql(
-			"""select account, debit, credit from `tabGL Entry`
-			where voucher_type='Purchase Invoice' and voucher_no = %s
-			order by account""",
-			pi.name,
-		)
+		gle = get_gl_entries("Purchase Invoice", pi.name)
 		self.assertSequenceEqual(gle, expected_gle)
 
 		pi.cancel()
@@ -264,12 +259,7 @@
 			),
 		)
 
-		gle = frappe.db.sql(
-			"""select account, debit, credit from `tabGL Entry`
-			where voucher_type='Journal Entry' and voucher_no = %s
-			order by account""",
-			asset.journal_entry_for_scrap,
-		)
+		gle = get_gl_entries("Journal Entry", asset.journal_entry_for_scrap)
 		self.assertSequenceEqual(gle, expected_gle)
 
 		restore_asset(asset.name)
@@ -345,13 +335,7 @@
 			("Debtors - _TC", 25000.0, 0.0),
 		)
 
-		gle = frappe.db.sql(
-			"""select account, debit, credit from `tabGL Entry`
-			where voucher_type='Sales Invoice' and voucher_no = %s
-			order by account""",
-			si.name,
-		)
-
+		gle = get_gl_entries("Sales Invoice", si.name)
 		self.assertSequenceEqual(gle, expected_gle)
 
 		si.cancel()
@@ -425,13 +409,7 @@
 			("Debtors - _TC", 40000.0, 0.0),
 		)
 
-		gle = frappe.db.sql(
-			"""select account, debit, credit from `tabGL Entry`
-			where voucher_type='Sales Invoice' and voucher_no = %s
-			order by account""",
-			si.name,
-		)
-
+		gle = get_gl_entries("Sales Invoice", si.name)
 		self.assertSequenceEqual(gle, expected_gle)
 
 	def test_asset_with_maintenance_required_status_after_sale(self):
@@ -572,13 +550,7 @@
 			("CWIP Account - _TC", 5250.0, 0.0),
 		)
 
-		pr_gle = frappe.db.sql(
-			"""select account, debit, credit from `tabGL Entry`
-			where voucher_type='Purchase Receipt' and voucher_no = %s
-			order by account""",
-			pr.name,
-		)
-
+		pr_gle = get_gl_entries("Purchase Receipt", pr.name)
 		self.assertSequenceEqual(pr_gle, expected_gle)
 
 		pi = make_invoice(pr.name)
@@ -591,13 +563,7 @@
 			("Creditors - _TC", 0.0, 5500.0),
 		)
 
-		pi_gle = frappe.db.sql(
-			"""select account, debit, credit from `tabGL Entry`
-			where voucher_type='Purchase Invoice' and voucher_no = %s
-			order by account""",
-			pi.name,
-		)
-
+		pi_gle = get_gl_entries("Purchase Invoice", pi.name)
 		self.assertSequenceEqual(pi_gle, expected_gle)
 
 		asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name")
@@ -624,13 +590,7 @@
 
 		expected_gle = (("_Test Fixed Asset - _TC", 5250.0, 0.0), ("CWIP Account - _TC", 0.0, 5250.0))
 
-		gle = frappe.db.sql(
-			"""select account, debit, credit from `tabGL Entry`
-			where voucher_type='Asset' and voucher_no = %s
-			order by account""",
-			asset_doc.name,
-		)
-
+		gle = get_gl_entries("Asset", asset_doc.name)
 		self.assertSequenceEqual(gle, expected_gle)
 
 	def test_asset_cwip_toggling_cases(self):
@@ -653,10 +613,7 @@
 		asset_doc.available_for_use_date = nowdate()
 		asset_doc.calculate_depreciation = 0
 		asset_doc.submit()
-		gle = frappe.db.sql(
-			"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
-			asset_doc.name,
-		)
+		gle = get_gl_entries("Asset", asset_doc.name)
 		self.assertFalse(gle)
 
 		# case 1 -- PR with cwip disabled, Asset with cwip enabled
@@ -670,10 +627,7 @@
 		asset_doc.available_for_use_date = nowdate()
 		asset_doc.calculate_depreciation = 0
 		asset_doc.submit()
-		gle = frappe.db.sql(
-			"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
-			asset_doc.name,
-		)
+		gle = get_gl_entries("Asset", asset_doc.name)
 		self.assertFalse(gle)
 
 		# case 2 -- PR with cwip enabled, Asset with cwip disabled
@@ -686,10 +640,7 @@
 		asset_doc.available_for_use_date = nowdate()
 		asset_doc.calculate_depreciation = 0
 		asset_doc.submit()
-		gle = frappe.db.sql(
-			"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
-			asset_doc.name,
-		)
+		gle = get_gl_entries("Asset", asset_doc.name)
 		self.assertTrue(gle)
 
 		# case 3 -- PI with cwip disabled, Asset with cwip enabled
@@ -702,10 +653,7 @@
 		asset_doc.available_for_use_date = nowdate()
 		asset_doc.calculate_depreciation = 0
 		asset_doc.submit()
-		gle = frappe.db.sql(
-			"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
-			asset_doc.name,
-		)
+		gle = get_gl_entries("Asset", asset_doc.name)
 		self.assertFalse(gle)
 
 		# case 4 -- PI with cwip enabled, Asset with cwip disabled
@@ -718,10 +666,7 @@
 		asset_doc.available_for_use_date = nowdate()
 		asset_doc.calculate_depreciation = 0
 		asset_doc.submit()
-		gle = frappe.db.sql(
-			"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
-			asset_doc.name,
-		)
+		gle = get_gl_entries("Asset", asset_doc.name)
 		self.assertTrue(gle)
 
 		frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip)
@@ -1055,7 +1000,11 @@
 			},
 		)
 
-		depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0])
+		asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
+
+		depreciation_amount = get_depreciation_amount(
+			asset_depr_schedule_doc, asset, 100000, asset.finance_books[0]
+		)
 		self.assertEqual(depreciation_amount, 30000)
 
 	def test_make_depr_schedule(self):
@@ -1701,6 +1650,30 @@
 
 		self.assertRaises(frappe.ValidationError, jv.insert)
 
+	def test_multi_currency_asset_pr_creation(self):
+		pr = make_purchase_receipt(
+			item_code="Macbook Pro",
+			qty=1,
+			rate=100.0,
+			location="Test Location",
+			supplier="_Test Supplier USD",
+			currency="USD",
+		)
+
+		pr.submit()
+		self.assertTrue(get_gl_entries("Purchase Receipt", pr.name))
+
+
+def get_gl_entries(doctype, docname):
+	gl_entry = frappe.qb.DocType("GL Entry")
+	return (
+		frappe.qb.from_(gl_entry)
+		.select(gl_entry.account, gl_entry.debit, gl_entry.credit)
+		.where((gl_entry.voucher_type == doctype) & (gl_entry.voucher_no == docname))
+		.orderby(gl_entry.account)
+		.run()
+	)
+
 
 def create_asset_data():
 	if not frappe.db.exists("Asset Category", "Computers"):
@@ -1763,6 +1736,7 @@
 				"expected_value_after_useful_life": args.expected_value_after_useful_life or 0,
 				"depreciation_start_date": args.depreciation_start_date,
 				"daily_prorata_based": args.daily_prorata_based or 0,
+				"shift_based": args.shift_based or 0,
 			},
 		)
 
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
index 3d2dff1..c99297d 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
@@ -8,11 +8,13 @@
 	},
 
 	make_schedules_editable: function(frm) {
-		var is_editable = frm.doc.depreciation_method == "Manual" ? true : false;
+		var is_manual_hence_editable = frm.doc.depreciation_method === "Manual" ? true : false;
+		var is_shift_hence_editable = frm.doc.shift_based ? true : false;
 
-		frm.toggle_enable("depreciation_schedule", is_editable);
-		frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_editable);
-		frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_editable);
+		frm.toggle_enable("depreciation_schedule", is_manual_hence_editable || is_shift_hence_editable);
+		frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_manual_hence_editable);
+		frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_manual_hence_editable);
+		frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", is_shift_hence_editable);
 	}
 });
 
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
index 8d8b463..be35914 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
@@ -20,6 +20,7 @@
   "total_number_of_depreciations",
   "rate_of_depreciation",
   "daily_prorata_based",
+  "shift_based",
   "column_break_8",
   "frequency_of_depreciation",
   "expected_value_after_useful_life",
@@ -184,12 +185,20 @@
    "label": "Depreciate based on daily pro-rata",
    "print_hide": 1,
    "read_only": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "eval:doc.depreciation_method == \"Straight Line\"",
+   "fieldname": "shift_based",
+   "fieldtype": "Check",
+   "label": "Depreciate based on shifts",
+   "read_only": 1
   }
  ],
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2023-11-03 21:32:15.021796",
+ "modified": "2023-11-29 00:57:00.461998",
  "modified_by": "Administrator",
  "module": "Assets",
  "name": "Asset Depreciation Schedule",
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index 7305691..6e390ce 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -26,6 +26,7 @@
 			self.prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(
 				self.asset, self.finance_book
 			)
+		self.update_shift_depr_schedule()
 
 	def validate(self):
 		self.validate_another_asset_depr_schedule_does_not_exist()
@@ -73,6 +74,16 @@
 	def on_cancel(self):
 		self.db_set("status", "Cancelled")
 
+	def update_shift_depr_schedule(self):
+		if not self.shift_based or self.docstatus != 0:
+			return
+
+		asset_doc = frappe.get_doc("Asset", self.asset)
+		fb_row = asset_doc.finance_books[self.finance_book_id - 1]
+
+		self.make_depr_schedule(asset_doc, fb_row)
+		self.set_accumulated_depreciation(asset_doc, fb_row)
+
 	def prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(self, asset_name, fb_name):
 		asset_doc = frappe.get_doc("Asset", asset_name)
 
@@ -154,13 +165,14 @@
 		self.rate_of_depreciation = row.rate_of_depreciation
 		self.expected_value_after_useful_life = row.expected_value_after_useful_life
 		self.daily_prorata_based = row.daily_prorata_based
+		self.shift_based = row.shift_based
 		self.status = "Draft"
 
 	def make_depr_schedule(
 		self,
 		asset_doc,
 		row,
-		date_of_disposal,
+		date_of_disposal=None,
 		update_asset_finance_book_row=True,
 		value_after_depreciation=None,
 	):
@@ -181,6 +193,8 @@
 		num_of_depreciations_completed = 0
 		depr_schedule = []
 
+		self.schedules_before_clearing = self.get("depreciation_schedule")
+
 		for schedule in self.get("depreciation_schedule"):
 			if schedule.journal_entry:
 				num_of_depreciations_completed += 1
@@ -246,6 +260,7 @@
 				prev_depreciation_amount = 0
 
 			depreciation_amount = get_depreciation_amount(
+				self,
 				asset_doc,
 				value_after_depreciation,
 				row,
@@ -282,10 +297,7 @@
 				)
 
 				if depreciation_amount > 0:
-					self.add_depr_schedule_row(
-						date_of_disposal,
-						depreciation_amount,
-					)
+					self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n)
 
 				break
 
@@ -369,10 +381,7 @@
 				skip_row = True
 
 			if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0:
-				self.add_depr_schedule_row(
-					schedule_date,
-					depreciation_amount,
-				)
+				self.add_depr_schedule_row(schedule_date, depreciation_amount, n)
 
 	# to ensure that final accumulated depreciation amount is accurate
 	def get_adjusted_depreciation_amount(
@@ -394,16 +403,22 @@
 	def get_depreciation_amount_for_first_row(self):
 		return self.get("depreciation_schedule")[0].depreciation_amount
 
-	def add_depr_schedule_row(
-		self,
-		schedule_date,
-		depreciation_amount,
-	):
+	def add_depr_schedule_row(self, schedule_date, depreciation_amount, schedule_idx):
+		if self.shift_based:
+			shift = (
+				self.schedules_before_clearing[schedule_idx].shift
+				if self.schedules_before_clearing and len(self.schedules_before_clearing) > schedule_idx
+				else frappe.get_cached_value("Asset Shift Factor", {"default": 1}, "shift_name")
+			)
+		else:
+			shift = None
+
 		self.append(
 			"depreciation_schedule",
 			{
 				"schedule_date": schedule_date,
 				"depreciation_amount": depreciation_amount,
+				"shift": shift,
 			},
 		)
 
@@ -445,6 +460,7 @@
 				and i == max(straight_line_idx) - 1
 				and not date_of_disposal
 				and not date_of_return
+				and not row.shift_based
 			):
 				depreciation_amount += flt(
 					value_after_depreciation - flt(row.expected_value_after_useful_life),
@@ -527,6 +543,7 @@
 
 
 def get_depreciation_amount(
+	asset_depr_schedule,
 	asset,
 	depreciable_value,
 	fb_row,
@@ -537,7 +554,7 @@
 ):
 	if fb_row.depreciation_method in ("Straight Line", "Manual"):
 		return get_straight_line_or_manual_depr_amount(
-			asset, fb_row, schedule_idx, number_of_pending_depreciations
+			asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
 		)
 	else:
 		rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
@@ -559,8 +576,11 @@
 
 
 def get_straight_line_or_manual_depr_amount(
-	asset, row, schedule_idx, number_of_pending_depreciations
+	asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations
 ):
+	if row.shift_based:
+		return get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx)
+
 	# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
 	if asset.flags.increase_in_asset_life:
 		return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
@@ -655,6 +675,41 @@
 			) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
 
 
+def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
+	if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation:
+		return (
+			flt(asset.gross_purchase_amount)
+			- flt(asset.opening_accumulated_depreciation)
+			- flt(row.expected_value_after_useful_life)
+		) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
+
+	asset_shift_factors_map = get_asset_shift_factors_map()
+	shift = (
+		asset_depr_schedule.schedules_before_clearing[schedule_idx].shift
+		if len(asset_depr_schedule.schedules_before_clearing) > schedule_idx
+		else None
+	)
+	shift_factor = asset_shift_factors_map.get(shift) if shift else 0
+
+	shift_factors_sum = sum(
+		flt(asset_shift_factors_map.get(schedule.shift))
+		for schedule in asset_depr_schedule.schedules_before_clearing
+	)
+
+	return (
+		(
+			flt(asset.gross_purchase_amount)
+			- flt(asset.opening_accumulated_depreciation)
+			- flt(row.expected_value_after_useful_life)
+		)
+		/ flt(shift_factors_sum)
+	) * shift_factor
+
+
+def get_asset_shift_factors_map():
+	return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True))
+
+
 def get_wdv_or_dd_depr_amount(
 	depreciable_value,
 	rate_of_depreciation,
@@ -803,7 +858,12 @@
 
 
 def get_temp_asset_depr_schedule_doc(
-	asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False
+	asset_doc,
+	row,
+	date_of_disposal=None,
+	date_of_return=None,
+	update_asset_finance_book_row=False,
+	new_depr_schedule=None,
 ):
 	current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
 		asset_doc.name, "Active", row.finance_book
@@ -818,6 +878,21 @@
 
 	temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
 
+	if new_depr_schedule:
+		temp_asset_depr_schedule_doc.depreciation_schedule = []
+
+		for schedule in new_depr_schedule:
+			temp_asset_depr_schedule_doc.append(
+				"depreciation_schedule",
+				{
+					"schedule_date": schedule.schedule_date,
+					"depreciation_amount": schedule.depreciation_amount,
+					"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
+					"journal_entry": schedule.journal_entry,
+					"shift": schedule.shift,
+				},
+			)
+
 	temp_asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(
 		asset_doc,
 		row,
@@ -839,6 +914,7 @@
 	return asset_depr_schedule_doc.get("depreciation_schedule")
 
 
+@frappe.whitelist()
 def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
 	asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book)
 
diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
index e597d5f..25ae7a4 100644
--- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
+++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
@@ -9,6 +9,7 @@
   "depreciation_method",
   "total_number_of_depreciations",
   "daily_prorata_based",
+  "shift_based",
   "column_break_5",
   "frequency_of_depreciation",
   "depreciation_start_date",
@@ -97,12 +98,19 @@
    "fieldname": "daily_prorata_based",
    "fieldtype": "Check",
    "label": "Depreciate based on daily pro-rata"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval:doc.depreciation_method == \"Straight Line\"",
+   "fieldname": "shift_based",
+   "fieldtype": "Check",
+   "label": "Depreciate based on shifts"
   }
  ],
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-11-03 21:30:24.266601",
+ "modified": "2023-11-29 00:57:07.579777",
  "modified_by": "Administrator",
  "module": "Assets",
  "name": "Asset Finance Book",
diff --git a/erpnext/accounts/doctype/unreconcile_payments/__init__.py b/erpnext/assets/doctype/asset_shift_allocation/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/unreconcile_payments/__init__.py
copy to erpnext/assets/doctype/asset_shift_allocation/__init__.py
diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js
new file mode 100644
index 0000000..54df693
--- /dev/null
+++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js
@@ -0,0 +1,14 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+frappe.ui.form.on('Asset Shift Allocation', {
+	onload: function(frm) {
+		frm.events.make_schedules_editable(frm);
+	},
+
+	make_schedules_editable: function(frm) {
+		frm.toggle_enable("depreciation_schedule", true);
+		frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", false);
+		frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", false);
+		frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", true);
+	}
+});
diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json
new file mode 100644
index 0000000..89fa298
--- /dev/null
+++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json
@@ -0,0 +1,111 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2023-11-24 15:07:44.652133",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+  "section_break_esaa",
+  "asset",
+  "naming_series",
+  "column_break_tdae",
+  "finance_book",
+  "amended_from",
+  "depreciation_schedule_section",
+  "depreciation_schedule"
+ ],
+ "fields": [
+  {
+   "fieldname": "section_break_esaa",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Asset Shift Allocation",
+   "print_hide": 1,
+   "read_only": 1,
+   "search_index": 1
+  },
+  {
+   "fieldname": "asset",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Asset",
+   "options": "Asset",
+   "reqd": 1
+  },
+  {
+   "fieldname": "column_break_tdae",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "finance_book",
+   "fieldtype": "Link",
+   "label": "Finance Book",
+   "options": "Finance Book"
+  },
+  {
+   "depends_on": "eval:!doc.__islocal",
+   "fieldname": "depreciation_schedule_section",
+   "fieldtype": "Section Break",
+   "label": "Depreciation Schedule"
+  },
+  {
+   "fieldname": "depreciation_schedule",
+   "fieldtype": "Table",
+   "label": "Depreciation Schedule",
+   "options": "Depreciation Schedule"
+  },
+  {
+   "fieldname": "naming_series",
+   "fieldtype": "Select",
+   "label": "Naming Series",
+   "options": "ACC-ASA-.YYYY.-",
+   "reqd": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-11-29 04:05:04.683518",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Shift Allocation",
+ "naming_rule": "By \"Naming Series\" field",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  },
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts User",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py
new file mode 100644
index 0000000..d419ef4
--- /dev/null
+++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py
@@ -0,0 +1,262 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import (
+	add_months,
+	cint,
+	flt,
+	get_last_day,
+	get_link_to_form,
+	is_last_day_of_the_month,
+)
+
+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_shift_factors_map,
+	get_temp_asset_depr_schedule_doc,
+)
+
+
+class AssetShiftAllocation(Document):
+	def after_insert(self):
+		self.fetch_and_set_depr_schedule()
+
+	def validate(self):
+		self.asset_depr_schedule_doc = get_asset_depr_schedule_doc(
+			self.asset, "Active", self.finance_book
+		)
+
+		self.validate_invalid_shift_change()
+		self.update_depr_schedule()
+
+	def on_submit(self):
+		self.create_new_asset_depr_schedule()
+
+	def fetch_and_set_depr_schedule(self):
+		if self.asset_depr_schedule_doc:
+			if self.asset_depr_schedule_doc.shift_based:
+				for schedule in self.asset_depr_schedule_doc.get("depreciation_schedule"):
+					self.append(
+						"depreciation_schedule",
+						{
+							"schedule_date": schedule.schedule_date,
+							"depreciation_amount": schedule.depreciation_amount,
+							"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
+							"journal_entry": schedule.journal_entry,
+							"shift": schedule.shift,
+						},
+					)
+
+				self.flags.ignore_validate = True
+				self.save()
+			else:
+				frappe.throw(
+					_(
+						"Asset Depreciation Schedule for Asset {0} and Finance Book {1} is not using shift based depreciation"
+					).format(self.asset, self.finance_book)
+				)
+		else:
+			frappe.throw(
+				_("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
+					self.asset, self.finance_book
+				)
+			)
+
+	def validate_invalid_shift_change(self):
+		if not self.get("depreciation_schedule") or self.docstatus == 1:
+			return
+
+		for i, sch in enumerate(self.depreciation_schedule):
+			if (
+				sch.journal_entry and self.asset_depr_schedule_doc.depreciation_schedule[i].shift != sch.shift
+			):
+				frappe.throw(
+					_(
+						"Row {0}: Shift cannot be changed since the depreciation has already been processed"
+					).format(i)
+				)
+
+	def update_depr_schedule(self):
+		if not self.get("depreciation_schedule") or self.docstatus == 1:
+			return
+
+		self.allocate_shift_diff_in_depr_schedule()
+
+		asset_doc = frappe.get_doc("Asset", self.asset)
+		fb_row = asset_doc.finance_books[self.asset_depr_schedule_doc.finance_book_id - 1]
+
+		asset_doc.flags.shift_allocation = True
+
+		temp_depr_schedule = get_temp_asset_depr_schedule_doc(
+			asset_doc, fb_row, new_depr_schedule=self.depreciation_schedule
+		).get("depreciation_schedule")
+
+		self.depreciation_schedule = []
+
+		for schedule in temp_depr_schedule:
+			self.append(
+				"depreciation_schedule",
+				{
+					"schedule_date": schedule.schedule_date,
+					"depreciation_amount": schedule.depreciation_amount,
+					"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
+					"journal_entry": schedule.journal_entry,
+					"shift": schedule.shift,
+				},
+			)
+
+	def allocate_shift_diff_in_depr_schedule(self):
+		asset_shift_factors_map = get_asset_shift_factors_map()
+		reverse_asset_shift_factors_map = {
+			asset_shift_factors_map[k]: k for k in asset_shift_factors_map
+		}
+
+		original_shift_factors_sum = sum(
+			flt(asset_shift_factors_map.get(schedule.shift))
+			for schedule in self.asset_depr_schedule_doc.depreciation_schedule
+		)
+
+		new_shift_factors_sum = sum(
+			flt(asset_shift_factors_map.get(schedule.shift)) for schedule in self.depreciation_schedule
+		)
+
+		diff = new_shift_factors_sum - original_shift_factors_sum
+
+		if diff > 0:
+			for i, schedule in reversed(list(enumerate(self.depreciation_schedule))):
+				if diff <= 0:
+					break
+
+				shift_factor = flt(asset_shift_factors_map.get(schedule.shift))
+
+				if shift_factor <= diff:
+					self.depreciation_schedule.pop()
+					diff -= shift_factor
+				else:
+					try:
+						self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(
+							shift_factor - diff
+						)
+						diff = 0
+					except Exception:
+						frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format(
+							shift_factor - diff
+						)
+		elif diff < 0:
+			shift_factors = list(asset_shift_factors_map.values())
+			desc_shift_factors = sorted(shift_factors, reverse=True)
+			depr_schedule_len_diff = self.asset_depr_schedule_doc.total_number_of_depreciations - len(
+				self.depreciation_schedule
+			)
+			subsets_result = []
+
+			if depr_schedule_len_diff > 0:
+				num_rows_to_add = depr_schedule_len_diff
+
+				while not subsets_result and num_rows_to_add > 0:
+					find_subsets_with_sum(shift_factors, num_rows_to_add, abs(diff), [], subsets_result)
+					if subsets_result:
+						break
+					num_rows_to_add -= 1
+
+				if subsets_result:
+					for i in range(num_rows_to_add):
+						schedule_date = add_months(
+							self.depreciation_schedule[-1].schedule_date,
+							cint(self.asset_depr_schedule_doc.frequency_of_depreciation),
+						)
+
+						if is_last_day_of_the_month(self.depreciation_schedule[-1].schedule_date):
+							schedule_date = get_last_day(schedule_date)
+
+						self.append(
+							"depreciation_schedule",
+							{
+								"schedule_date": schedule_date,
+								"shift": reverse_asset_shift_factors_map.get(subsets_result[0][i]),
+							},
+						)
+
+			if depr_schedule_len_diff <= 0 or not subsets_result:
+				for i, schedule in reversed(list(enumerate(self.depreciation_schedule))):
+					diff = abs(diff)
+
+					if diff <= 0:
+						break
+
+					shift_factor = flt(asset_shift_factors_map.get(schedule.shift))
+
+					if shift_factor <= diff:
+						for sf in desc_shift_factors:
+							if sf - shift_factor <= diff:
+								self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(sf)
+								diff -= sf - shift_factor
+								break
+					else:
+						try:
+							self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(
+								shift_factor + diff
+							)
+							diff = 0
+						except Exception:
+							frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format(
+								shift_factor + diff
+							)
+
+	def create_new_asset_depr_schedule(self):
+		new_asset_depr_schedule_doc = frappe.copy_doc(self.asset_depr_schedule_doc)
+
+		new_asset_depr_schedule_doc.depreciation_schedule = []
+
+		for schedule in self.depreciation_schedule:
+			new_asset_depr_schedule_doc.append(
+				"depreciation_schedule",
+				{
+					"schedule_date": schedule.schedule_date,
+					"depreciation_amount": schedule.depreciation_amount,
+					"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
+					"journal_entry": schedule.journal_entry,
+					"shift": schedule.shift,
+				},
+			)
+
+		notes = _(
+			"This schedule was created when Asset {0}'s shifts were adjusted through Asset Shift Allocation {1}."
+		).format(
+			get_link_to_form("Asset", self.asset),
+			get_link_to_form(self.doctype, self.name),
+		)
+
+		new_asset_depr_schedule_doc.notes = notes
+
+		self.asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
+		self.asset_depr_schedule_doc.cancel()
+
+		new_asset_depr_schedule_doc.submit()
+
+		add_asset_activity(
+			self.asset,
+			_("Asset's depreciation schedule updated after Asset Shift Allocation {0}").format(
+				get_link_to_form(self.doctype, self.name)
+			),
+		)
+
+
+def find_subsets_with_sum(numbers, k, target_sum, current_subset, result):
+	if k == 0 and target_sum == 0:
+		result.append(current_subset.copy())
+		return
+	if k <= 0 or target_sum <= 0 or not numbers:
+		return
+
+	# Include the current number in the subset
+	find_subsets_with_sum(
+		numbers, k - 1, target_sum - numbers[0], current_subset + [numbers[0]], result
+	)
+
+	# Exclude the current number from the subset
+	find_subsets_with_sum(numbers[1:], k, target_sum, current_subset, result)
diff --git a/erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py b/erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py
new file mode 100644
index 0000000..8d00a24
--- /dev/null
+++ b/erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py
@@ -0,0 +1,113 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import cstr
+
+from erpnext.assets.doctype.asset.test_asset import create_asset
+from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
+	get_depr_schedule,
+)
+
+
+class TestAssetShiftAllocation(FrappeTestCase):
+	@classmethod
+	def setUpClass(cls):
+		create_asset_shift_factors()
+
+	@classmethod
+	def tearDownClass(cls):
+		frappe.db.rollback()
+
+	def test_asset_shift_allocation(self):
+		asset = create_asset(
+			calculate_depreciation=1,
+			available_for_use_date="2023-01-01",
+			purchase_date="2023-01-01",
+			gross_purchase_amount=120000,
+			depreciation_start_date="2023-01-31",
+			total_number_of_depreciations=12,
+			frequency_of_depreciation=1,
+			shift_based=1,
+			submit=1,
+		)
+
+		expected_schedules = [
+			["2023-01-31", 10000.0, 10000.0, "Single"],
+			["2023-02-28", 10000.0, 20000.0, "Single"],
+			["2023-03-31", 10000.0, 30000.0, "Single"],
+			["2023-04-30", 10000.0, 40000.0, "Single"],
+			["2023-05-31", 10000.0, 50000.0, "Single"],
+			["2023-06-30", 10000.0, 60000.0, "Single"],
+			["2023-07-31", 10000.0, 70000.0, "Single"],
+			["2023-08-31", 10000.0, 80000.0, "Single"],
+			["2023-09-30", 10000.0, 90000.0, "Single"],
+			["2023-10-31", 10000.0, 100000.0, "Single"],
+			["2023-11-30", 10000.0, 110000.0, "Single"],
+			["2023-12-31", 10000.0, 120000.0, "Single"],
+		]
+
+		schedules = [
+			[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift]
+			for d in get_depr_schedule(asset.name, "Active")
+		]
+
+		self.assertEqual(schedules, expected_schedules)
+
+		asset_shift_allocation = frappe.get_doc(
+			{"doctype": "Asset Shift Allocation", "asset": asset.name}
+		).insert()
+
+		schedules = [
+			[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift]
+			for d in asset_shift_allocation.get("depreciation_schedule")
+		]
+
+		self.assertEqual(schedules, expected_schedules)
+
+		asset_shift_allocation = frappe.get_doc("Asset Shift Allocation", asset_shift_allocation.name)
+		asset_shift_allocation.depreciation_schedule[4].shift = "Triple"
+		asset_shift_allocation.save()
+
+		schedules = [
+			[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift]
+			for d in asset_shift_allocation.get("depreciation_schedule")
+		]
+
+		expected_schedules = [
+			["2023-01-31", 10000.0, 10000.0, "Single"],
+			["2023-02-28", 10000.0, 20000.0, "Single"],
+			["2023-03-31", 10000.0, 30000.0, "Single"],
+			["2023-04-30", 10000.0, 40000.0, "Single"],
+			["2023-05-31", 20000.0, 60000.0, "Triple"],
+			["2023-06-30", 10000.0, 70000.0, "Single"],
+			["2023-07-31", 10000.0, 80000.0, "Single"],
+			["2023-08-31", 10000.0, 90000.0, "Single"],
+			["2023-09-30", 10000.0, 100000.0, "Single"],
+			["2023-10-31", 10000.0, 110000.0, "Single"],
+			["2023-11-30", 10000.0, 120000.0, "Single"],
+		]
+
+		self.assertEqual(schedules, expected_schedules)
+
+		asset_shift_allocation.submit()
+
+		schedules = [
+			[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift]
+			for d in get_depr_schedule(asset.name, "Active")
+		]
+
+		self.assertEqual(schedules, expected_schedules)
+
+
+def create_asset_shift_factors():
+	shifts = [
+		{"doctype": "Asset Shift Factor", "shift_name": "Half", "shift_factor": 0.5, "default": 0},
+		{"doctype": "Asset Shift Factor", "shift_name": "Single", "shift_factor": 1, "default": 1},
+		{"doctype": "Asset Shift Factor", "shift_name": "Double", "shift_factor": 1.5, "default": 0},
+		{"doctype": "Asset Shift Factor", "shift_name": "Triple", "shift_factor": 2, "default": 0},
+	]
+
+	for s in shifts:
+		frappe.get_doc(s).insert()
diff --git a/erpnext/accounts/doctype/unreconcile_payments/__init__.py b/erpnext/assets/doctype/asset_shift_factor/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/unreconcile_payments/__init__.py
copy to erpnext/assets/doctype/asset_shift_factor/__init__.py
diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js
new file mode 100644
index 0000000..88887fe
--- /dev/null
+++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.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 Shift Factor", {
+// 	refresh(frm) {
+
+// 	},
+// });
diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json
new file mode 100644
index 0000000..fd04ffc
--- /dev/null
+++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json
@@ -0,0 +1,74 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:shift_name",
+ "creation": "2023-11-27 18:16:03.980086",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+  "shift_name",
+  "shift_factor",
+  "default"
+ ],
+ "fields": [
+  {
+   "fieldname": "shift_name",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Shift Name",
+   "reqd": 1,
+   "unique": 1
+  },
+  {
+   "fieldname": "shift_factor",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Shift Factor",
+   "reqd": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "default",
+   "fieldtype": "Check",
+   "in_list_view": 1,
+   "label": "Default"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2023-11-29 04:04:24.272872",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Shift Factor",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  },
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts User",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py
new file mode 100644
index 0000000..4c275ce
--- /dev/null
+++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py
@@ -0,0 +1,24 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+
+class AssetShiftFactor(Document):
+	def validate(self):
+		self.validate_default()
+
+	def validate_default(self):
+		if self.default:
+			existing_default_shift_factor = frappe.db.get_value(
+				"Asset Shift Factor", {"default": 1}, "name"
+			)
+
+			if existing_default_shift_factor:
+				frappe.throw(
+					_("Asset Shift Factor {0} is set as default currently. Please change it first.").format(
+						frappe.bold(existing_default_shift_factor)
+					)
+				)
diff --git a/erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py b/erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py
new file mode 100644
index 0000000..7507367
--- /dev/null
+++ b/erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.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 TestAssetShiftFactor(FrappeTestCase):
+	pass
diff --git a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
index 884e0c6..ef706e8 100644
--- a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
+++ b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
@@ -12,6 +12,7 @@
   "column_break_3",
   "accumulated_depreciation_amount",
   "journal_entry",
+  "shift",
   "make_depreciation_entry"
  ],
  "fields": [
@@ -57,11 +58,17 @@
    "fieldname": "make_depreciation_entry",
    "fieldtype": "Button",
    "label": "Make Depreciation Entry"
+  },
+  {
+   "fieldname": "shift",
+   "fieldtype": "Link",
+   "label": "Shift",
+   "options": "Asset Shift Factor"
   }
  ],
  "istable": 1,
  "links": [],
- "modified": "2023-07-26 12:56:48.718736",
+ "modified": "2023-11-27 18:28:35.325376",
  "modified_by": "Administrator",
  "module": "Assets",
  "name": "Depreciation Schedule",
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 0599992..0af93bf 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -17,6 +17,7 @@
   "po_required",
   "pr_required",
   "blanket_order_allowance",
+  "project_update_frequency",
   "column_break_12",
   "maintain_same_rate",
   "set_landed_cost_based_on_purchase_invoice_rate",
@@ -172,6 +173,14 @@
    "fieldname": "blanket_order_allowance",
    "fieldtype": "Float",
    "label": "Blanket Order Allowance (%)"
+  },
+  {
+   "default": "Each Transaction",
+   "description": "How often should Project be updated of Total Purchase Cost ?",
+   "fieldname": "project_update_frequency",
+   "fieldtype": "Select",
+   "label": "Update frequency of Project",
+   "options": "Each Transaction\nManual"
   }
  ],
  "icon": "fa fa-cog",
@@ -179,7 +188,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2023-10-25 14:03:32.520418",
+ "modified": "2023-11-24 10:55:51.287327",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Buying Settings",
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 55c01e8..0f8574c 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -16,7 +16,7 @@
 	make_purchase_invoice as make_pi_from_po,
 )
 from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
-from erpnext.controllers.accounts_controller import update_child_qty_rate
+from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
 from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
 from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.doctype.material_request.material_request import make_purchase_order
@@ -27,6 +27,21 @@
 
 
 class TestPurchaseOrder(FrappeTestCase):
+	def test_purchase_order_qty(self):
+		po = create_purchase_order(qty=1, do_not_save=True)
+		po.append(
+			"items",
+			{
+				"item_code": "_Test Item",
+				"qty": -1,
+				"rate": 10,
+			},
+		)
+		self.assertRaises(frappe.NonNegativeError, po.save)
+
+		po.items[1].qty = 0
+		self.assertRaises(InvalidQtyError, po.save)
+
 	def test_make_purchase_receipt(self):
 		po = create_purchase_order(do_not_submit=True)
 		self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 2d706f4..98c1b38 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -214,6 +214,7 @@
    "fieldtype": "Float",
    "in_list_view": 1,
    "label": "Quantity",
+   "non_negative": 1,
    "oldfieldname": "qty",
    "oldfieldtype": "Currency",
    "print_width": "60px",
@@ -917,7 +918,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-11-14 18:34:27.267382",
+ "modified": "2023-11-24 13:24:41.298416",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Purchase Order Item",
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index 31bf439..b052f56 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -165,16 +165,17 @@
 @frappe.validate_and_sanitize_search_inputs
 def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
 	supplier = filters.get("supplier")
-	return frappe.db.sql(
-		"""
-		SELECT
-			`tabContact`.name from `tabContact`,
-			`tabDynamic Link`
-		WHERE
-			`tabContact`.name = `tabDynamic Link`.parent
-			and `tabDynamic Link`.link_name = %(supplier)s
-			and `tabDynamic Link`.link_doctype = 'Supplier'
-			and `tabContact`.name like %(txt)s
-		""",
-		{"supplier": supplier, "txt": "%%%s%%" % txt},
-	)
+	contact = frappe.qb.DocType("Contact")
+	dynamic_link = frappe.qb.DocType("Dynamic Link")
+
+	return (
+		frappe.qb.from_(contact)
+		.join(dynamic_link)
+		.on(contact.name == dynamic_link.parent)
+		.select(contact.name, contact.email_id)
+		.where(
+			(dynamic_link.link_name == supplier)
+			& (dynamic_link.link_doctype == "Supplier")
+			& (contact.name.like("%{0}%".format(txt)))
+		)
+	).run(as_dict=False)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 38c1e82..f551133 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -71,6 +71,10 @@
 	pass
 
 
+class InvalidQtyError(frappe.ValidationError):
+	pass
+
+
 force_item_fields = (
 	"item_group",
 	"brand",
@@ -239,7 +243,7 @@
 				references_map.setdefault(x.parent, []).append(x.name)
 
 			for doc, rows in references_map.items():
-				unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc)
+				unreconcile_doc = frappe.get_doc("Unreconcile Payment", doc)
 				for row in rows:
 					unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0])
 
@@ -248,9 +252,9 @@
 				unreconcile_doc.save(ignore_permissions=True)
 
 		# delete docs upon parent doc deletion
-		unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name})
+		unreconcile_docs = frappe.db.get_all("Unreconcile Payment", filters={"voucher_no": self.name})
 		for x in unreconcile_docs:
-			_doc = frappe.get_doc("Unreconcile Payments", x.name)
+			_doc = frappe.get_doc("Unreconcile Payment", x.name)
 			if _doc.docstatus == 1:
 				_doc.cancel()
 			_doc.delete()
@@ -625,6 +629,7 @@
 
 					args["doctype"] = self.doctype
 					args["name"] = self.name
+					args["child_doctype"] = item.doctype
 					args["child_docname"] = item.name
 					args["ignore_pricing_rule"] = (
 						self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
@@ -910,10 +915,16 @@
 			return flt(args.get(field, 0) / self.get("conversion_rate", 1))
 
 	def validate_qty_is_not_zero(self):
-		if self.doctype != "Purchase Receipt":
-			for item in self.items:
-				if not item.qty:
-					frappe.throw(_("Item quantity can not be zero"))
+		if self.doctype == "Purchase Receipt":
+			return
+
+		for item in self.items:
+			if not flt(item.qty):
+				frappe.throw(
+					msg=_("Row #{0}: Item quantity cannot be zero").format(item.idx),
+					title=_("Invalid Quantity"),
+					exc=InvalidQtyError,
+				)
 
 	def validate_account_currency(self, account, account_currency=None):
 		valid_currency = [self.company_currency]
@@ -3141,16 +3152,19 @@
 		conv_fac_precision = child_item.precision("conversion_factor") or 2
 		qty_precision = child_item.precision("qty") or 2
 
-		if flt(child_item.billed_amt, rate_precision) > flt(
-			flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision
-		):
+		# Amount cannot be lesser than billed amount, except for negative amounts
+		row_rate = flt(d.get("rate"), rate_precision)
+		amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
+			row_rate * flt(d.get("qty"), qty_precision), rate_precision
+		)
+		if amount_below_billed_amt and row_rate > 0.0:
 			frappe.throw(
 				_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.").format(
 					child_item.idx, child_item.item_code
 				)
 			)
 		else:
-			child_item.rate = flt(d.get("rate"), rate_precision)
+			child_item.rate = row_rate
 
 		if d.get("conversion_factor"):
 			if child_item.stock_uom == child_item.uom:
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index d34fbeb..5575a24 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -350,11 +350,12 @@
 		return il
 
 	def has_product_bundle(self, item_code):
-		return frappe.db.sql(
-			"""select name from `tabProduct Bundle`
-			where new_item_code=%s and docstatus != 2""",
-			item_code,
-		)
+		product_bundle = frappe.qb.DocType("Product Bundle")
+		return (
+			frappe.qb.from_(product_bundle)
+			.select(product_bundle.name)
+			.where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
+		).run()
 
 	def get_already_delivered_qty(self, current_docname, so, so_detail):
 		delivered_via_dn = frappe.db.sql(
diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py
index 6b61ae9..47762ac 100644
--- a/erpnext/controllers/tests/test_subcontracting_controller.py
+++ b/erpnext/controllers/tests/test_subcontracting_controller.py
@@ -1001,6 +1001,7 @@
 		"Subcontracted Item SA5": {},
 		"Subcontracted Item SA6": {},
 		"Subcontracted Item SA7": {},
+		"Subcontracted Item SA8": {},
 	}
 
 	for item, properties in sub_contracted_items.items():
@@ -1020,6 +1021,7 @@
 		},
 		"Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
 		"Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"},
+		"Subcontracted SRM Item 8": {},
 	}
 
 	for item, properties in raw_materials.items():
@@ -1043,6 +1045,7 @@
 		"Subcontracted Service Item 5": {},
 		"Subcontracted Service Item 6": {},
 		"Subcontracted Service Item 7": {},
+		"Subcontracted Service Item 8": {},
 	}
 
 	for item, properties in service_items.items():
@@ -1066,6 +1069,7 @@
 		"Subcontracted Item SA5": ["Subcontracted SRM Item 5"],
 		"Subcontracted Item SA6": ["Subcontracted SRM Item 3"],
 		"Subcontracted Item SA7": ["Subcontracted SRM Item 1"],
+		"Subcontracted Item SA8": ["Subcontracted SRM Item 8"],
 	}
 
 	for item_code, raw_materials in boms.items():
diff --git a/erpnext/crm/doctype/competitor/competitor.json b/erpnext/crm/doctype/competitor/competitor.json
index 280441f..fd6da23 100644
--- a/erpnext/crm/doctype/competitor/competitor.json
+++ b/erpnext/crm/doctype/competitor/competitor.json
@@ -29,8 +29,16 @@
   }
  ],
  "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-10-21 12:43:59.106807",
+ "links": [
+  {
+   "is_child_table": 1,
+   "link_doctype": "Competitor Detail",
+   "link_fieldname": "competitor",
+   "parent_doctype": "Quotation",
+   "table_fieldname": "competitors"
+  }
+ ],
+ "modified": "2023-11-23 19:33:54.284279",
  "modified_by": "Administrator",
  "module": "CRM",
  "name": "Competitor",
@@ -64,5 +72,6 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index fdec88d..d22cc55 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -36,6 +36,15 @@
 	def before_insert(self):
 		self.contact_doc = None
 		if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"):
+			if self.source == "Existing Customer" and self.customer:
+				contact = frappe.db.get_value(
+					"Dynamic Link",
+					{"link_doctype": "Customer", "link_name": self.customer},
+					"parent",
+				)
+				if contact:
+					self.contact_doc = frappe.get_doc("Contact", contact)
+					return
 			self.contact_doc = self.create_contact()
 
 	def after_insert(self):
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index c6ab6f1..857471f 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -419,7 +419,6 @@
 		"erpnext.projects.doctype.project.project.collect_project_status",
 	],
 	"hourly_long": [
-		"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
 		"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
 		"erpnext.utilities.bulk_transaction.retry",
 	],
@@ -450,6 +449,7 @@
 		"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
 	],
 	"daily_long": [
+		"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
 		"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",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index db6bc80..f303531 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -185,7 +185,8 @@
 			# override capacity for employee
 			production_capacity = 1
 
-		if time_logs and production_capacity > len(time_logs):
+		overlap_count = self.get_overlap_count(time_logs)
+		if time_logs and production_capacity > overlap_count:
 			return {}
 
 		if self.workstation_type and time_logs:
@@ -195,6 +196,37 @@
 
 		return time_logs[-1]
 
+	@staticmethod
+	def get_overlap_count(time_logs):
+		count = 1
+
+		# Check overlap exists or not between the overlapping time logs with the current Job Card
+		for idx, row in enumerate(time_logs):
+			next_idx = idx
+			if idx + 1 < len(time_logs):
+				next_idx = idx + 1
+				next_row = time_logs[next_idx]
+				if row.name == next_row.name:
+					continue
+
+				if (
+					(
+						get_datetime(next_row.from_time) >= get_datetime(row.from_time)
+						and get_datetime(next_row.from_time) <= get_datetime(row.to_time)
+					)
+					or (
+						get_datetime(next_row.to_time) >= get_datetime(row.from_time)
+						and get_datetime(next_row.to_time) <= get_datetime(row.to_time)
+					)
+					or (
+						get_datetime(next_row.from_time) <= get_datetime(row.from_time)
+						and get_datetime(next_row.to_time) >= get_datetime(row.to_time)
+					)
+				):
+					count += 1
+
+		return count
+
 	def get_time_logs(self, args, doctype, check_next_available_slot=False):
 		jc = frappe.qb.DocType("Job Card")
 		jctl = frappe.qb.DocType(doctype)
@@ -211,7 +243,14 @@
 		query = (
 			frappe.qb.from_(jctl)
 			.from_(jc)
-			.select(jc.name.as_("name"), jctl.from_time, jctl.to_time, jc.workstation, jc.workstation_type)
+			.select(
+				jc.name.as_("name"),
+				jctl.name.as_("row_name"),
+				jctl.from_time,
+				jctl.to_time,
+				jc.workstation,
+				jc.workstation_type,
+			)
 			.where(
 				(jctl.parent == jc.name)
 				& (Criterion.any(time_conditions))
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 0ae7657..e2c8f07 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -921,6 +921,20 @@
 			"Test RM Item 2 for Scrap Item Test",
 		]
 
+		from_time = add_days(now(), -1)
+		job_cards = frappe.get_all(
+			"Job Card Time Log",
+			fields=["distinct parent as name", "docstatus"],
+			filters={"from_time": (">", from_time)},
+			order_by="creation asc",
+		)
+
+		for job_card in job_cards:
+			if job_card.docstatus == 1:
+				frappe.get_doc("Job Card", job_card.name).cancel()
+
+			frappe.delete_doc("Job Card Time Log", job_card.name)
+
 		company = "_Test Company with perpetual inventory"
 		for item_code in items:
 			create_item(
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 0badab5..a73502d 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -259,6 +259,7 @@
 erpnext.patches.v15_0.saudi_depreciation_warning
 erpnext.patches.v15_0.delete_saudi_doctypes
 erpnext.patches.v14_0.show_loan_management_deprecation_warning
+erpnext.patches.v14_0.clear_reconciliation_values_from_singles
 execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
 
 [post_model_sync]
@@ -344,13 +345,12 @@
 erpnext.patches.v15_0.update_sre_from_voucher_details
 erpnext.patches.v14_0.rename_over_order_allowance_field
 erpnext.patches.v14_0.migrate_delivery_stop_lock_field
-execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50)
-execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50)
 erpnext.patches.v14_0.add_default_for_repost_settings
 erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month
 erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based
 erpnext.patches.v15_0.set_reserved_stock_in_bin
 erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation
 erpnext.patches.v14_0.update_zero_asset_quantity_field
+execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
 # below migration patch should always run last
 erpnext.patches.v14_0.migrate_gl_to_payment_ledger
diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
index e53bdf8..08ddbbf 100644
--- a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
+++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
@@ -21,6 +21,9 @@
 	params = set({x.casefold(): x for x in params}.values())
 
 	for parameter in params:
+		if frappe.db.exists("Quality Inspection Parameter", parameter):
+			continue
+
 		frappe.get_doc(
 			{"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter}
 		).insert(ignore_permissions=True)
diff --git a/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py
new file mode 100644
index 0000000..c1f5b60
--- /dev/null
+++ b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py
@@ -0,0 +1,17 @@
+from frappe import qb
+
+
+def execute():
+	"""
+	Clear `tabSingles` and Payment Reconciliation tables of values
+	"""
+	singles = qb.DocType("Singles")
+	qb.from_(singles).delete().where(singles.doctype == "Payment Reconciliation").run()
+	doctypes = [
+		"Payment Reconciliation Invoice",
+		"Payment Reconciliation Payment",
+		"Payment Reconciliation Allocation",
+	]
+	for x in doctypes:
+		dt = qb.DocType(x)
+		qb.from_(dt).delete().run()
diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
index 9a2a39f..793497b 100644
--- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
+++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
@@ -86,6 +86,7 @@
 			afb.frequency_of_depreciation,
 			afb.rate_of_depreciation,
 			afb.expected_value_after_useful_life,
+			afb.shift_based,
 		)
 		.where(asset.docstatus < 2)
 		.orderby(afb.idx)
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index f366f77..2dac399 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -68,6 +68,10 @@
 				frm.events.create_duplicate(frm);
 			}, __("Actions"));
 
+			frm.add_custom_button(__('Update Total Purchase Cost'), () => {
+				frm.events.update_total_purchase_cost(frm);
+			}, __("Actions"));
+
 			frm.trigger("set_project_status_button");
 
 
@@ -92,6 +96,22 @@
 
 	},
 
+	update_total_purchase_cost: function(frm) {
+		frappe.call({
+			method: "erpnext.projects.doctype.project.project.recalculate_project_total_purchase_cost",
+			args: {project: frm.doc.name},
+			freeze: true,
+			freeze_message: __('Recalculating Purchase Cost against this Project...'),
+			callback: function(r) {
+				if (r && !r.exc) {
+					frappe.msgprint(__('Total Purchase Cost has been updated'));
+					frm.refresh();
+				}
+			}
+
+		});
+	},
+
 	set_project_status_button: function(frm) {
 		frm.add_custom_button(__('Set Project Status'), () => {
 			let d = new frappe.ui.Dialog({
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index e9aed1a..4f2e395 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -4,11 +4,11 @@
 
 import frappe
 from email_reply_parser import EmailReplyParser
-from frappe import _
+from frappe import _, qb
 from frappe.desk.reportview import get_match_cond
 from frappe.model.document import Document
 from frappe.query_builder import Interval
-from frappe.query_builder.functions import Count, CurDate, Date, UnixTimestamp
+from frappe.query_builder.functions import Count, CurDate, Date, Sum, UnixTimestamp
 from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today
 from frappe.utils.user import is_website_user
 
@@ -249,12 +249,7 @@
 			self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100
 
 	def update_purchase_costing(self):
-		total_purchase_cost = frappe.db.sql(
-			"""select sum(base_net_amount)
-			from `tabPurchase Invoice Item` where project = %s and docstatus=1""",
-			self.name,
-		)
-
+		total_purchase_cost = calculate_total_purchase_cost(self.name)
 		self.total_purchase_cost = total_purchase_cost and total_purchase_cost[0][0] or 0
 
 	def update_sales_amount(self):
@@ -695,3 +690,29 @@
 
 def get_users_email(doc):
 	return [d.email for d in doc.users if frappe.db.get_value("User", d.user, "enabled")]
+
+
+def calculate_total_purchase_cost(project: str | None = None):
+	if project:
+		pitem = qb.DocType("Purchase Invoice Item")
+		frappe.qb.DocType("Purchase Invoice Item")
+		total_purchase_cost = (
+			qb.from_(pitem)
+			.select(Sum(pitem.base_net_amount))
+			.where((pitem.project == project) & (pitem.docstatus == 1))
+			.run(as_list=True)
+		)
+		return total_purchase_cost
+	return None
+
+
+@frappe.whitelist()
+def recalculate_project_total_purchase_cost(project: str | None = None):
+	if project:
+		total_purchase_cost = calculate_total_purchase_cost(project)
+		frappe.db.set_value(
+			"Project",
+			project,
+			"total_purchase_cost",
+			(total_purchase_cost and total_purchase_cost[0][0] or 0),
+		)
diff --git a/erpnext/public/js/communication.js b/erpnext/public/js/communication.js
index 7ce8b09..f205d88 100644
--- a/erpnext/public/js/communication.js
+++ b/erpnext/public/js/communication.js
@@ -13,7 +13,7 @@
 				frappe.confirm(__(confirm_msg, [__("Issue")]), () => {
 					frm.trigger('make_issue_from_communication');
 				})
-			}, "Create");
+			}, __("Create"));
 		}
 
 		if(!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 2c40f49..6dc24fa 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -512,6 +512,7 @@
 							cost_center: item.cost_center,
 							tax_category: me.frm.doc.tax_category,
 							item_tax_template: item.item_tax_template,
+							child_doctype: item.doctype,
 							child_docname: item.name,
 							is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow,
 						}
diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js
index fa00ed2..79490a1 100644
--- a/erpnext/public/js/utils/unreconcile.js
+++ b/erpnext/public/js/utils/unreconcile.js
@@ -1,6 +1,6 @@
 frappe.provide('erpnext.accounts');
 
-erpnext.accounts.unreconcile_payments = {
+erpnext.accounts.unreconcile_payment = {
 	add_unreconcile_btn(frm) {
 		if (frm.doc.docstatus == 1) {
 			if(((frm.doc.doctype == "Journal Entry") && (frm.doc.voucher_type != "Journal Entry"))
@@ -10,7 +10,7 @@
 			}
 
 			frappe.call({
-				"method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references",
+				"method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.doc_has_references",
 				"args": {
 					"doctype": frm.doc.doctype,
 					"docname": frm.doc.name
@@ -18,7 +18,7 @@
 				callback: function(r) {
 					if (r.message) {
 						frm.add_custom_button(__("UnReconcile"), function() {
-							erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm);
+							erpnext.accounts.unreconcile_payment.build_unreconcile_dialog(frm);
 						}, __('Actions'));
 					}
 				}
@@ -74,7 +74,7 @@
 
 			// get linked payments
 			frappe.call({
-				"method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc",
+				"method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.get_linked_payments_for_doc",
 				"args": {
 					"company": frm.doc.company,
 					"doctype": frm.doc.doctype,
@@ -96,8 +96,8 @@
 
 								let selected_allocations = values.allocations.filter(x=>x.__checked);
 								if (selected_allocations.length > 0) {
-									let selection_map = erpnext.accounts.unreconcile_payments.build_selection_map(frm, selected_allocations);
-									erpnext.accounts.unreconcile_payments.create_unreconcile_docs(selection_map);
+									let selection_map = erpnext.accounts.unreconcile_payment.build_selection_map(frm, selected_allocations);
+									erpnext.accounts.unreconcile_payment.create_unreconcile_docs(selection_map);
 									d.hide();
 
 								} else {
@@ -115,7 +115,7 @@
 
 	create_unreconcile_docs(selection_map) {
 		frappe.call({
-			"method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.create_unreconcile_doc_for_selection",
+			"method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.create_unreconcile_doc_for_selection",
 			"args": {
 				"selections": selection_map
 			},
diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py
index 59ef58b..6ef21e5 100644
--- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py
+++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py
@@ -141,7 +141,7 @@
 		return frappe.db.sql(
 			"""
 			select
-				s.vat_emirate as emirate, sum(i.base_amount) as total, sum(i.tax_amount)
+				s.vat_emirate as emirate, sum(i.base_net_amount) as total, sum(i.tax_amount)
 			from
 				`tabSales Invoice Item` i inner join `tabSales Invoice` s
 			on
@@ -356,7 +356,7 @@
 			frappe.db.sql(
 				"""
 			select
-				sum(i.base_amount) as total
+				sum(i.base_net_amount) as total
 			from
 				`tabSales Invoice Item` i inner join `tabSales Invoice` s
 			on
@@ -383,7 +383,7 @@
 			frappe.db.sql(
 				"""
 			select
-				sum(i.base_amount) as total
+				sum(i.base_net_amount) as total
 			from
 				`tabSales Invoice Item` i inner join `tabSales Invoice` s
 			on
diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json
index 56155fb..c4f21b6 100644
--- a/erpnext/selling/doctype/product_bundle/product_bundle.json
+++ b/erpnext/selling/doctype/product_bundle/product_bundle.json
@@ -1,315 +1,119 @@
 {
- "allow_copy": 0, 
- "allow_guest_to_view": 0, 
- "allow_import": 1, 
- "allow_rename": 0, 
- "beta": 0, 
- "creation": "2013-06-20 11:53:21", 
- "custom": 0, 
- "description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials", 
- "docstatus": 0, 
- "doctype": "DocType", 
- "document_type": "", 
- "editable_grid": 0, 
+ "actions": [],
+ "allow_import": 1,
+ "creation": "2013-06-20 11:53:21",
+ "description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+  "basic_section",
+  "new_item_code",
+  "description",
+  "column_break_eonk",
+  "disabled",
+  "item_section",
+  "items",
+  "section_break_4",
+  "about"
+ ],
  "fields": [
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "basic_section", 
-   "fieldtype": "Section Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "basic_section",
+   "fieldtype": "Section Break"
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "description": "", 
-   "fieldname": "new_item_code", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 1, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Parent Item", 
-   "length": 0, 
-   "no_copy": 1, 
-   "oldfieldname": "new_item_code", 
-   "oldfieldtype": "Data", 
-   "options": "Item", 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 1, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "new_item_code",
+   "fieldtype": "Link",
+   "in_global_search": 1,
+   "in_list_view": 1,
+   "label": "Parent Item",
+   "no_copy": 1,
+   "oldfieldname": "new_item_code",
+   "oldfieldtype": "Data",
+   "options": "Item",
+   "reqd": 1
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "description", 
-   "fieldtype": "Data", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Description", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "description",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Description"
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "description": "List items that form the package.", 
-   "fieldname": "item_section", 
-   "fieldtype": "Section Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Items", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "description": "List items that form the package.",
+   "fieldname": "item_section",
+   "fieldtype": "Section Break",
+   "label": "Items"
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "items", 
-   "fieldtype": "Table", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Items", 
-   "length": 0, 
-   "no_copy": 0, 
-   "oldfieldname": "sales_bom_items", 
-   "oldfieldtype": "Table", 
-   "options": "Product Bundle Item", 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 1, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "items",
+   "fieldtype": "Table",
+   "label": "Items",
+   "oldfieldname": "sales_bom_items",
+   "oldfieldtype": "Table",
+   "options": "Product Bundle Item",
+   "reqd": 1
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "section_break_4", 
-   "fieldtype": "Section Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "section_break_4",
+   "fieldtype": "Section Break"
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "about", 
-   "fieldtype": "HTML", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "<h3>About Product Bundle</h3>\n\n<p>Aggregate group of <b>Items</b> into another <b>Item</b>. This is useful if you are bundling a certain <b>Items</b> into a package and you maintain stock of the packed <b>Items</b> and not the aggregate <b>Item</b>.</p>\n<p>The package <b>Item</b> will have <code>Is Stock Item</code> as <b>No</b> and <code>Is Sales Item</code> as <b>Yes</b>.</p>\n<h4>Example:</h4>\n<p>If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.</p>", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
+   "fieldname": "about",
+   "fieldtype": "HTML",
+   "options": "<h3>About Product Bundle</h3>\n\n<p>Aggregate group of <b>Items</b> into another <b>Item</b>. This is useful if you are bundling a certain <b>Items</b> into a package and you maintain stock of the packed <b>Items</b> and not the aggregate <b>Item</b>.</p>\n<p>The package <b>Item</b> will have <code>Is Stock Item</code> as <b>No</b> and <code>Is Sales Item</code> as <b>Yes</b>.</p>\n<h4>Example:</h4>\n<p>If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.</p>"
+  },
+  {
+   "default": "0",
+   "fieldname": "disabled",
+   "fieldtype": "Check",
+   "label": "Disabled"
+  },
+  {
+   "fieldname": "column_break_eonk",
+   "fieldtype": "Column Break"
   }
- ], 
- "has_web_view": 0, 
- "hide_heading": 0, 
- "hide_toolbar": 0, 
- "icon": "fa fa-sitemap", 
- "idx": 1, 
- "image_view": 0, 
- "in_create": 0, 
- "is_submittable": 0, 
- "issingle": 0, 
- "istable": 0, 
- "max_attachments": 0, 
- "modified": "2020-09-18 17:26:09.703215", 
- "modified_by": "Administrator", 
- "module": "Selling", 
- "name": "Product Bundle", 
- "owner": "Administrator", 
+ ],
+ "icon": "fa fa-sitemap",
+ "idx": 1,
+ "links": [],
+ "modified": "2023-11-22 15:20:46.805114",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Product Bundle",
+ "owner": "Administrator",
  "permissions": [
   {
-   "amend": 0, 
-   "apply_user_permissions": 0, 
-   "cancel": 0, 
-   "create": 1, 
-   "delete": 1, 
-   "email": 1, 
-   "export": 0, 
-   "if_owner": 0, 
-   "import": 0, 
-   "permlevel": 0, 
-   "print": 1, 
-   "read": 1, 
-   "report": 1, 
-   "role": "Stock Manager", 
-   "set_user_permissions": 0, 
-   "share": 1, 
-   "submit": 0, 
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Stock Manager",
+   "share": 1,
    "write": 1
-  }, 
+  },
   {
-   "amend": 0, 
-   "apply_user_permissions": 0, 
-   "cancel": 0, 
-   "create": 0, 
-   "delete": 0, 
-   "email": 1, 
-   "export": 0, 
-   "if_owner": 0, 
-   "import": 0, 
-   "permlevel": 0, 
-   "print": 1, 
-   "read": 1, 
-   "report": 1, 
-   "role": "Stock User", 
-   "set_user_permissions": 0, 
-   "share": 0, 
-   "submit": 0, 
-   "write": 0
-  }, 
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Stock User"
+  },
   {
-   "amend": 0, 
-   "apply_user_permissions": 0, 
-   "cancel": 0, 
-   "create": 1, 
-   "delete": 1, 
-   "email": 1, 
-   "export": 0, 
-   "if_owner": 0, 
-   "import": 0, 
-   "permlevel": 0, 
-   "print": 1, 
-   "read": 1, 
-   "report": 1, 
-   "role": "Sales User", 
-   "set_user_permissions": 0, 
-   "share": 1, 
-   "submit": 0, 
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Sales User",
+   "share": 1,
    "write": 1
   }
- ], 
- "quick_entry": 0, 
- "read_only": 0, 
- "read_only_onload": 0, 
- "show_name_in_global_search": 0, 
- "sort_order": "ASC", 
- "track_changes": 0, 
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py
index ac83c0f..3d4ffeb 100644
--- a/erpnext/selling/doctype/product_bundle/product_bundle.py
+++ b/erpnext/selling/doctype/product_bundle/product_bundle.py
@@ -59,10 +59,12 @@
 		"""Validates, main Item is not a stock item"""
 		if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"):
 			frappe.throw(_("Parent Item {0} must not be a Stock Item").format(self.new_item_code))
+		if frappe.db.get_value("Item", self.new_item_code, "is_fixed_asset"):
+			frappe.throw(_("Parent Item {0} must not be a Fixed Asset").format(self.new_item_code))
 
 	def validate_child_items(self):
 		for item in self.items:
-			if frappe.db.exists("Product Bundle", item.item_code):
+			if frappe.db.exists("Product Bundle", {"name": item.item_code, "disabled": 0}):
 				frappe.throw(
 					_(
 						"Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save"
@@ -73,12 +75,20 @@
 @frappe.whitelist()
 @frappe.validate_and_sanitize_search_inputs
 def get_new_item_code(doctype, txt, searchfield, start, page_len, filters):
-	from erpnext.controllers.queries import get_match_cond
+	product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name")
 
-	return frappe.db.sql(
-		"""select name, item_name, description from tabItem
-		where is_stock_item=0 and name not in (select name from `tabProduct Bundle`)
-		and %s like %s %s limit %s offset %s"""
-		% (searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
-		("%%%s%%" % txt, page_len, start),
+	item = frappe.qb.DocType("Item")
+	query = (
+		frappe.qb.from_(item)
+		.select(item.item_code, item.item_name)
+		.where(
+			(item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%"))
+		)
+		.limit(page_len)
+		.offset(start)
 	)
+
+	if product_bundles:
+		query = query.where(item.name.notin(product_bundles))
+
+	return query.run()
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 3ad18da..97b214e 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -214,13 +214,12 @@
 					label: __("Items to Reserve"),
 					allow_bulk_edit: false,
 					cannot_add_rows: true,
-					cannot_delete_rows: true,
 					data: [],
 					fields: [
 						{
-							fieldname: "name",
+							fieldname: "sales_order_item",
 							fieldtype: "Data",
-							label: __("Name"),
+							label: __("Sales Order Item"),
 							reqd: 1,
 							read_only: 1,
 						},
@@ -260,7 +259,7 @@
 			],
 			primary_action_label: __("Reserve Stock"),
 			primary_action: () => {
-				var data = {items: dialog.fields_dict.items.grid.get_selected_children()};
+				var data = {items: dialog.fields_dict.items.grid.data};
 
 				if (data.items && data.items.length > 0) {
 					frappe.call({
@@ -278,9 +277,6 @@
 						}
 					});
 				}
-				else {
-					frappe.msgprint(__("Please select items to reserve."));
-				}
 
 				dialog.hide();
 			},
@@ -292,7 +288,7 @@
 
 				if (unreserved_qty > 0) {
 					dialog.fields_dict.items.df.data.push({
-						'name': item.name,
+						'sales_order_item': item.name,
 						'item_code': item.item_code,
 						'warehouse': item.warehouse,
 						'qty_to_reserve': (unreserved_qty / flt(item.conversion_factor))
@@ -308,7 +304,7 @@
 	cancel_stock_reservation_entries(frm) {
 		const dialog = new frappe.ui.Dialog({
 			title: __("Stock Unreservation"),
-			size: "large",
+			size: "extra-large",
 			fields: [
 				{
 					fieldname: "sr_entries",
@@ -316,14 +312,13 @@
 					label: __("Reserved Stock"),
 					allow_bulk_edit: false,
 					cannot_add_rows: true,
-					cannot_delete_rows: true,
 					in_place_edit: true,
 					data: [],
 					fields: [
 						{
-							fieldname: "name",
+							fieldname: "sre",
 							fieldtype: "Link",
-							label: __("SRE"),
+							label: __("Stock Reservation Entry"),
 							options: "Stock Reservation Entry",
 							reqd: 1,
 							read_only: 1,
@@ -360,14 +355,14 @@
 			],
 			primary_action_label: __("Unreserve Stock"),
 			primary_action: () => {
-				var data = {sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children()};
+				var data = {sr_entries: dialog.fields_dict.sr_entries.grid.data};
 
 				if (data.sr_entries && data.sr_entries.length > 0) {
 					frappe.call({
 						doc: frm.doc,
 						method: "cancel_stock_reservation_entries",
 						args: {
-							sre_list: data.sr_entries,
+							sre_list: data.sr_entries.map(item => item.sre),
 						},
 						freeze: true,
 						freeze_message: __('Unreserving Stock...'),
@@ -377,9 +372,6 @@
 						}
 					});
 				}
-				else {
-					frappe.msgprint(__("Please select items to unreserve."));
-				}
 
 				dialog.hide();
 			},
@@ -396,7 +388,7 @@
 					r.message.forEach(sre => {
 						if (flt(sre.reserved_qty) > flt(sre.delivered_qty)) {
 							dialog.fields_dict.sr_entries.df.data.push({
-								'name': sre.name,
+								'sre': sre.name,
 								'item_code': sre.item_code,
 								'warehouse': sre.warehouse,
 								'qty': (flt(sre.reserved_qty) - flt(sre.delivered_qty))
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index a97198a..a23599b 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -688,7 +688,9 @@
 			"Sales Order Item": {
 				"doctype": "Material Request Item",
 				"field_map": {"name": "sales_order_item", "parent": "sales_order"},
-				"condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code)
+				"condition": lambda item: not frappe.db.exists(
+					"Product Bundle", {"name": item.item_code, "disabled": 0}
+				)
 				and get_remaining_qty(item) > 0,
 				"postprocess": update_item,
 			},
@@ -1309,7 +1311,7 @@
 
 
 def is_product_bundle(item_code):
-	return frappe.db.exists("Product Bundle", item_code)
+	return frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0})
 
 
 @frappe.whitelist()
@@ -1521,7 +1523,7 @@
 		product_bundle_parents = [
 			pb.new_item_code
 			for pb in frappe.get_all(
-				"Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
+				"Product Bundle", {"new_item_code": ["in", item_codes], "disabled": 0}, ["new_item_code"]
 			)
 		]
 
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index d8b5878..a518597 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -51,6 +51,35 @@
 	def tearDown(self):
 		frappe.set_user("Administrator")
 
+	def test_sales_order_with_negative_rate(self):
+		"""
+		Test if negative rate is allowed in Sales Order via doc submission and update items
+		"""
+		so = make_sales_order(qty=1, rate=100, do_not_save=True)
+		so.append("items", {"item_code": "_Test Item", "qty": 1, "rate": -10})
+		so.save()
+		so.submit()
+
+		first_item = so.get("items")[0]
+		second_item = so.get("items")[1]
+		trans_item = json.dumps(
+			[
+				{
+					"item_code": first_item.item_code,
+					"rate": first_item.rate,
+					"qty": first_item.qty,
+					"docname": first_item.name,
+				},
+				{
+					"item_code": second_item.item_code,
+					"rate": -20,
+					"qty": second_item.qty,
+					"docname": second_item.name,
+				},
+			]
+		)
+		update_child_qty_rate("Sales Order", trans_item, so.name)
+
 	def test_make_material_request(self):
 		so = make_sales_order(do_not_submit=True)
 
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index b4f7300..d4ccfc4 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -200,6 +200,7 @@
    "fieldtype": "Float",
    "in_list_view": 1,
    "label": "Quantity",
+   "non_negative": 1,
    "oldfieldname": "qty",
    "oldfieldtype": "Currency",
    "print_width": "100px",
@@ -895,7 +896,7 @@
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-11-14 18:37:12.787893",
+ "modified": "2023-11-24 13:24:55.756320",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Sales Order Item",
diff --git a/erpnext/accounts/doctype/unreconcile_payments/__init__.py b/erpnext/selling/report/lost_quotations/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/unreconcile_payments/__init__.py
copy to erpnext/selling/report/lost_quotations/__init__.py
diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.js b/erpnext/selling/report/lost_quotations/lost_quotations.js
new file mode 100644
index 0000000..78e76cb
--- /dev/null
+++ b/erpnext/selling/report/lost_quotations/lost_quotations.js
@@ -0,0 +1,40 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.query_reports["Lost Quotations"] = {
+	filters: [
+		{
+			fieldname: "company",
+			label: __("Company"),
+			fieldtype: "Link",
+			options: "Company",
+			default: frappe.defaults.get_user_default("Company"),
+		},
+		{
+			label: "Timespan",
+			fieldtype: "Select",
+			fieldname: "timespan",
+			options: [
+				"Last Week",
+				"Last Month",
+				"Last Quarter",
+				"Last 6 months",
+				"Last Year",
+				"This Week",
+				"This Month",
+				"This Quarter",
+				"This Year",
+			],
+			default: "This Year",
+			reqd: 1,
+		},
+		{
+			fieldname: "group_by",
+			label: __("Group By"),
+			fieldtype: "Select",
+			options: ["Lost Reason", "Competitor"],
+			default: "Lost Reason",
+			reqd: 1,
+		},
+	],
+};
diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.json b/erpnext/selling/report/lost_quotations/lost_quotations.json
new file mode 100644
index 0000000..8915bab
--- /dev/null
+++ b/erpnext/selling/report/lost_quotations/lost_quotations.json
@@ -0,0 +1,30 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2023-11-23 18:00:19.141922",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": null,
+ "letterhead": null,
+ "modified": "2023-11-23 19:27:28.854108",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Lost Quotations",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Quotation",
+ "report_name": "Lost Quotations",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Sales User"
+  },
+  {
+   "role": "Sales Manager"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.py b/erpnext/selling/report/lost_quotations/lost_quotations.py
new file mode 100644
index 0000000..7c0bfbd
--- /dev/null
+++ b/erpnext/selling/report/lost_quotations/lost_quotations.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from typing import Literal
+
+import frappe
+from frappe import _
+from frappe.model.docstatus import DocStatus
+from frappe.query_builder.functions import Coalesce, Count, Round, Sum
+from frappe.utils.data import get_timespan_date_range
+
+
+def execute(filters=None):
+	columns = get_columns(filters.get("group_by"))
+	from_date, to_date = get_timespan_date_range(filters.get("timespan").lower())
+	data = get_data(filters.get("company"), from_date, to_date, filters.get("group_by"))
+	return columns, data
+
+
+def get_columns(group_by: Literal["Lost Reason", "Competitor"]):
+	return [
+		{
+			"fieldname": "lost_reason" if group_by == "Lost Reason" else "competitor",
+			"label": _("Lost Reason") if group_by == "Lost Reason" else _("Competitor"),
+			"fieldtype": "Link",
+			"options": "Quotation Lost Reason" if group_by == "Lost Reason" else "Competitor",
+			"width": 200,
+		},
+		{
+			"filedname": "lost_quotations",
+			"label": _("Lost Quotations"),
+			"fieldtype": "Int",
+			"width": 150,
+		},
+		{
+			"filedname": "lost_quotations_pct",
+			"label": _("Lost Quotations %"),
+			"fieldtype": "Percent",
+			"width": 200,
+		},
+		{
+			"fieldname": "lost_value",
+			"label": _("Lost Value"),
+			"fieldtype": "Currency",
+			"width": 150,
+		},
+		{
+			"filedname": "lost_value_pct",
+			"label": _("Lost Value %"),
+			"fieldtype": "Percent",
+			"width": 200,
+		},
+	]
+
+
+def get_data(
+	company: str, from_date: str, to_date: str, group_by: Literal["Lost Reason", "Competitor"]
+):
+	"""Return quotation value grouped by lost reason or competitor"""
+	if group_by == "Lost Reason":
+		fieldname = "lost_reason"
+		dimension = frappe.qb.DocType("Quotation Lost Reason Detail")
+	elif group_by == "Competitor":
+		fieldname = "competitor"
+		dimension = frappe.qb.DocType("Competitor Detail")
+	else:
+		frappe.throw(_("Invalid Group By"))
+
+	q = frappe.qb.DocType("Quotation")
+
+	lost_quotation_condition = (
+		(q.status == "Lost")
+		& (q.docstatus == DocStatus.submitted())
+		& (q.transaction_date >= from_date)
+		& (q.transaction_date <= to_date)
+		& (q.company == company)
+	)
+
+	from_lost_quotations = frappe.qb.from_(q).where(lost_quotation_condition)
+	total_quotations = from_lost_quotations.select(Count(q.name))
+	total_value = from_lost_quotations.select(Sum(q.base_net_total))
+
+	query = (
+		frappe.qb.from_(q)
+		.select(
+			Coalesce(dimension[fieldname], _("Not Specified")),
+			Count(q.name).distinct(),
+			Round((Count(q.name).distinct() / total_quotations * 100), 2),
+			Sum(q.base_net_total),
+			Round((Sum(q.base_net_total) / total_value * 100), 2),
+		)
+		.left_join(dimension)
+		.on(dimension.parent == q.name)
+		.where(lost_quotation_condition)
+		.groupby(dimension[fieldname])
+	)
+
+	return query.run()
diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py
index 4fc20e6..6ed44ff 100644
--- a/erpnext/setup/doctype/email_digest/email_digest.py
+++ b/erpnext/setup/doctype/email_digest/email_digest.py
@@ -382,9 +382,10 @@
 		"""Get income to date"""
 		balance = 0.0
 		count = 0
+		fy_start_date = get_fiscal_year(self.future_to_date)[1]
 
 		for account in self.get_root_type_accounts(root_type):
-			balance += get_balance_on(account, date=self.future_to_date)
+			balance += get_balance_on(account, date=self.future_to_date, start_date=fy_start_date)
 			count += get_count_on(account, fieldname, date=self.future_to_date)
 
 		if fieldname == "income":
diff --git a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json
index 5d778ee..0eae08e 100644
--- a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json
+++ b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json
@@ -1,83 +1,58 @@
 {
- "allow_copy": 0, 
- "allow_import": 1, 
- "allow_rename": 0, 
- "autoname": "field:order_lost_reason", 
- "beta": 0, 
- "creation": "2013-01-10 16:34:24", 
- "custom": 0, 
- "docstatus": 0, 
- "doctype": "DocType", 
- "document_type": "Setup", 
- "editable_grid": 0, 
+ "actions": [],
+ "allow_import": 1,
+ "autoname": "field:order_lost_reason",
+ "creation": "2013-01-10 16:34:24",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+  "order_lost_reason"
+ ],
  "fields": [
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "fieldname": "order_lost_reason", 
-   "fieldtype": "Data", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_list_view": 1, 
-   "label": "Quotation Lost Reason", 
-   "length": 0, 
-   "no_copy": 0, 
-   "oldfieldname": "order_lost_reason", 
-   "oldfieldtype": "Data", 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "report_hide": 0, 
-   "reqd": 1, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
+   "fieldname": "order_lost_reason",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Quotation Lost Reason",
+   "oldfieldname": "order_lost_reason",
+   "oldfieldtype": "Data",
+   "reqd": 1,
+   "unique": 1
   }
- ], 
- "hide_heading": 0, 
- "hide_toolbar": 0, 
- "icon": "fa fa-flag", 
- "idx": 1, 
- "image_view": 0, 
- "in_create": 0, 
-
- "is_submittable": 0, 
- "issingle": 0, 
- "istable": 0, 
- "max_attachments": 0, 
- "modified": "2016-07-25 05:24:25.533953", 
- "modified_by": "Administrator", 
- "module": "Setup", 
- "name": "Quotation Lost Reason", 
- "owner": "Administrator", 
+ ],
+ "icon": "fa fa-flag",
+ "idx": 1,
+ "links": [
+  {
+   "is_child_table": 1,
+   "link_doctype": "Quotation Lost Reason Detail",
+   "link_fieldname": "lost_reason",
+   "parent_doctype": "Quotation",
+   "table_fieldname": "lost_reasons"
+  }
+ ],
+ "modified": "2023-11-23 19:31:02.743353",
+ "modified_by": "Administrator",
+ "module": "Setup",
+ "name": "Quotation Lost Reason",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
  "permissions": [
   {
-   "amend": 0, 
-   "apply_user_permissions": 0, 
-   "cancel": 0, 
-   "create": 1, 
-   "delete": 1, 
-   "email": 1, 
-   "export": 0, 
-   "if_owner": 0, 
-   "import": 0, 
-   "permlevel": 0, 
-   "print": 1, 
-   "read": 1, 
-   "report": 1, 
-   "role": "Sales Master Manager", 
-   "set_user_permissions": 0, 
-   "share": 1, 
-   "submit": 0, 
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Sales Master Manager",
+   "share": 1,
    "write": 1
   }
- ], 
- "quick_entry": 1, 
- "read_only": 0, 
- "read_only_onload": 0, 
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 66dd33a..f240136 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -615,7 +615,7 @@
 		items_list = [item.item_code for item in self.items]
 		return frappe.db.get_all(
 			"Product Bundle",
-			filters={"new_item_code": ["in", items_list]},
+			filters={"new_item_code": ["in", items_list], "disabled": 0},
 			pluck="name",
 		)
 
@@ -938,7 +938,7 @@
 				},
 				"postprocess": update_item,
 				"condition": lambda item: (
-					not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code})
+					not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0})
 					and flt(item.packed_qty) < flt(item.qty)
 				),
 			},
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 137c352..9465574 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -1247,6 +1247,25 @@
 		dn.reload()
 		self.assertFalse(dn.items[0].target_warehouse)
 
+	def test_serial_no_status(self):
+		from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+
+		item = make_item(
+			"Test Serial Item For Status",
+			{"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "TESTSERIAL.#####"},
+		)
+
+		item_code = item.name
+		pi = make_purchase_receipt(qty=1, item_code=item.name)
+		pi.reload()
+		serial_no = get_serial_nos_from_bundle(pi.items[0].serial_and_batch_bundle)
+
+		self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active")
+
+		dn = create_delivery_note(qty=1, item_code=item_code, serial_no=serial_no)
+		dn.reload()
+		self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
+
 
 def create_delivery_note(**args):
 	dn = frappe.new_doc("Delivery Note")
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index d8935fe..cb34497 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -512,8 +512,12 @@
 
 	def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
 		"Block merge if both old and new items have product bundles."
-		old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name})
-		new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name})
+		old_bundle = frappe.get_value(
+			"Product Bundle", filters={"new_item_code": old_name, "disabled": 0}
+		)
+		new_bundle = frappe.get_value(
+			"Product Bundle", filters={"new_item_code": new_name, "disabled": 0}
+		)
 
 		if old_bundle and new_bundle:
 			bundle_link = get_link_to_form("Product Bundle", old_bundle)
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index a9e9ad1..35701c9 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -55,7 +55,7 @@
 
 
 def is_product_bundle(item_code: str) -> bool:
-	return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code}))
+	return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code, "disabled": 0}))
 
 
 def get_indexed_packed_items_table(doc):
@@ -111,7 +111,7 @@
 			product_bundle_item.uom,
 			product_bundle_item.description,
 		)
-		.where(product_bundle.new_item_code == item_code)
+		.where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
 		.orderby(product_bundle_item.idx)
 	)
 	return query.run(as_dict=True)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index ed20209..e7f6204 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -233,7 +233,7 @@
 		for location in self.locations:
 			if location.warehouse and location.sales_order and location.sales_order_item:
 				item_details = {
-					"name": location.sales_order_item,
+					"sales_order_item": location.sales_order_item,
 					"item_code": location.item_code,
 					"warehouse": location.warehouse,
 					"qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)),
@@ -368,7 +368,9 @@
 				frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
 			if not cint(
 				frappe.get_cached_value("Item", item.item_code, "is_stock_item")
-			) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
+			) and not frappe.db.exists(
+				"Product Bundle", {"new_item_code": item.item_code, "disabled": 0}
+			):
 				continue
 			item_code = item.item_code
 			reference = item.sales_order_item or item.material_request_item
@@ -507,7 +509,9 @@
 		# bundle_item_code: Dict[component, qty]
 		product_bundle_qty_map = {}
 		for bundle_item_code in bundles:
-			bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code})
+			bundle = frappe.get_last_doc(
+				"Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0}
+			)
 			product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items}
 		return product_bundle_qty_map
 
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index d905fe5..8647528 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -571,7 +571,7 @@
 					)
 
 					stock_value_diff = (
-						flt(d.net_amount)
+						flt(d.base_net_amount)
 						+ flt(d.item_tax_amount / self.conversion_rate)
 						+ flt(d.landed_cost_voucher_amount)
 					)
@@ -731,12 +731,18 @@
 
 	def update_assets(self, item, valuation_rate):
 		assets = frappe.db.get_all(
-			"Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code}
+			"Asset",
+			filters={"purchase_receipt": self.name, "item_code": item.item_code},
+			fields=["name", "asset_quantity"],
 		)
 
 		for asset in assets:
-			frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(valuation_rate))
-			frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate))
+			frappe.db.set_value(
+				"Asset", asset.name, "gross_purchase_amount", flt(valuation_rate) * asset.asset_quantity
+			)
+			frappe.db.set_value(
+				"Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate) * asset.asset_quantity
+			)
 
 	def update_status(self, status):
 		self.set_status(update=True, status=status)
@@ -775,7 +781,7 @@
 		for item in self.items:
 			if item.sales_order and item.sales_order_item:
 				item_details = {
-					"name": item.sales_order_item,
+					"sales_order_item": item.sales_order_item,
 					"item_code": item.item_code,
 					"warehouse": item.warehouse,
 					"qty_to_reserve": item.stock_qty,
diff --git a/erpnext/stock/doctype/serial_no/serial_no.js b/erpnext/stock/doctype/serial_no/serial_no.js
index 9d5555e..1cb9fd1 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.js
+++ b/erpnext/stock/doctype/serial_no/serial_no.js
@@ -18,3 +18,22 @@
 frappe.ui.form.on("Serial No", "refresh", function(frm) {
 	frm.toggle_enable("item_code", frm.doc.__islocal);
 });
+
+
+frappe.ui.form.on("Serial No", {
+	refresh(frm) {
+		frm.trigger("view_ledgers")
+	},
+
+	view_ledgers(frm) {
+		frm.add_custom_button(__("View Ledgers"), () => {
+			frappe.route_options = {
+				"item_code": frm.doc.item_code,
+				"serial_no": frm.doc.name,
+				"posting_date": frappe.datetime.now_date(),
+				"posting_time": frappe.datetime.now_time()
+			};
+			frappe.set_route("query-report", "Serial No Ledger");
+		}).addClass('btn-primary');
+	}
+})
\ No newline at end of file
diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json
index ed1b0af..b4ece00 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.json
+++ b/erpnext/stock/doctype/serial_no/serial_no.json
@@ -269,7 +269,7 @@
    "in_list_view": 1,
    "in_standard_filter": 1,
    "label": "Status",
-   "options": "\nActive\nInactive\nExpired",
+   "options": "\nActive\nInactive\nDelivered\nExpired",
    "read_only": 1
   },
   {
@@ -280,7 +280,7 @@
  "icon": "fa fa-barcode",
  "idx": 1,
  "links": [],
- "modified": "2023-04-16 15:58:46.139887",
+ "modified": "2023-11-28 15:37:59.489945",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Serial No",
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
index 0954282..cbfa4e0 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -869,7 +869,7 @@
 	items = []
 	if items_details:
 		for item in items_details:
-			so_item = frappe.get_doc("Sales Order Item", item.get("name"))
+			so_item = frappe.get_doc("Sales Order Item", item.get("sales_order_item"))
 			so_item.warehouse = item.get("warehouse")
 			so_item.qty_to_reserve = (
 				flt(item.get("qty_to_reserve"))
@@ -1053,12 +1053,14 @@
 	from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
 	from_voucher_no: str = None,
 	from_voucher_detail_no: str = None,
-	sre_list: list[dict] = None,
+	sre_list: list = None,
 	notify: bool = True,
 ) -> None:
 	"""Cancel Stock Reservation Entries."""
 
 	if not sre_list:
+		sre_list = {}
+
 		if voucher_type and voucher_no:
 			sre_list = get_stock_reservation_entries_for_voucher(
 				voucher_type, voucher_no, voucher_detail_no, fields=["name"]
@@ -1082,9 +1084,11 @@
 
 			sre_list = query.run(as_dict=True)
 
+		sre_list = [d.name for d in sre_list]
+
 	if sre_list:
 		for sre in sre_list:
-			frappe.get_doc("Stock Reservation Entry", sre["name"]).cancel()
+			frappe.get_doc("Stock Reservation Entry", sre).cancel()
 
 		if notify:
 			msg = _("Stock Reservation Entries Cancelled")
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index c766cab..dfeb1ee 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -8,6 +8,7 @@
 from frappe import _, throw
 from frappe.model import child_table_fields, default_fields
 from frappe.model.meta import get_field_precision
+from frappe.model.utils import get_fetch_values
 from frappe.query_builder.functions import IfNull, Sum
 from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
 
@@ -149,7 +150,7 @@
 
 
 def set_valuation_rate(out, args):
-	if frappe.db.exists("Product Bundle", args.item_code, cache=True):
+	if frappe.db.exists("Product Bundle", {"name": args.item_code, "disabled": 0}, cache=True):
 		valuation_rate = 0.0
 		bundled_items = frappe.get_doc("Product Bundle", args.item_code)
 
@@ -571,6 +572,9 @@
 			item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
 			item_group = item_group_doc.parent_item_group
 
+	if args.child_doctype and item_tax_template:
+		out.update(get_fetch_values(args.child_doctype, "item_tax_template", item_tax_template))
+
 
 def _get_item_tax_template(args, taxes, out=None, for_validate=False):
 	if out is None:
diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py
index 7212b92..ae12fbb 100644
--- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py
+++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py
@@ -36,21 +36,27 @@
 			"fieldtype": "Link",
 			"fieldname": "company",
 			"options": "Company",
-			"width": 150,
+			"width": 120,
 		},
 		{
 			"label": _("Warehouse"),
 			"fieldtype": "Link",
 			"fieldname": "warehouse",
 			"options": "Warehouse",
-			"width": 150,
+			"width": 120,
+		},
+		{
+			"label": _("Status"),
+			"fieldtype": "Data",
+			"fieldname": "status",
+			"width": 120,
 		},
 		{
 			"label": _("Serial No"),
 			"fieldtype": "Link",
 			"fieldname": "serial_no",
 			"options": "Serial No",
-			"width": 150,
+			"width": 130,
 		},
 		{
 			"label": _("Valuation Rate"),
@@ -58,6 +64,12 @@
 			"fieldname": "valuation_rate",
 			"width": 150,
 		},
+		{
+			"label": _("Qty"),
+			"fieldtype": "Float",
+			"fieldname": "qty",
+			"width": 150,
+		},
 	]
 
 	return columns
@@ -83,12 +95,16 @@
 				"posting_time": row.posting_time,
 				"voucher_type": row.voucher_type,
 				"voucher_no": row.voucher_no,
+				"status": "Active" if row.actual_qty > 0 else "Delivered",
 				"company": row.company,
 				"warehouse": row.warehouse,
+				"qty": 1 if row.actual_qty > 0 else -1,
 			}
 		)
 
-		serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, [])
+		serial_nos = [{"serial_no": row.serial_no, "valuation_rate": row.valuation_rate}]
+		if row.serial_and_batch_bundle:
+			serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, [])
 
 		for index, bundle_data in enumerate(serial_nos):
 			if index == 0:
diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
index b1da3ec..416cf48 100644
--- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
+++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
@@ -166,4 +166,4 @@
 
 	if entries:
 		entries = ", ".join(entries)
-		frappe.msgprint(_(f"Reposting entries created: {entries}"))
+		frappe.msgprint(_("Reposting entries created: {0}").format(entries))
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index da98455..de28be1 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -255,11 +255,15 @@
 		if not serial_nos:
 			return
 
+		status = "Inactive"
+		if self.sle.actual_qty < 0:
+			status = "Delivered"
+
 		sn_table = frappe.qb.DocType("Serial No")
 		(
 			frappe.qb.update(sn_table)
 			.set(sn_table.warehouse, warehouse)
-			.set(sn_table.status, "Active" if warehouse else "Inactive")
+			.set(sn_table.status, "Active" if warehouse else status)
 			.where(sn_table.name.isin(serial_nos))
 		).run()
 
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
index faf0cad..70ca1c3 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
@@ -8,7 +8,7 @@
 
 from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
 from erpnext.controllers.subcontracting_controller import SubcontractingController
-from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
+from erpnext.stock.stock_balance import update_bin_qty
 from erpnext.stock.utils import get_bin
 
 
@@ -114,7 +114,32 @@
 			):
 				item_wh_list.append([item.item_code, item.warehouse])
 		for item_code, warehouse in item_wh_list:
-			update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)})
+			update_bin_qty(
+				item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)}
+			)
+
+	@staticmethod
+	def get_ordered_qty(item_code, warehouse):
+		table = frappe.qb.DocType("Subcontracting Order")
+		child = frappe.qb.DocType("Subcontracting Order Item")
+
+		query = (
+			frappe.qb.from_(table)
+			.inner_join(child)
+			.on(table.name == child.parent)
+			.select((child.qty - child.received_qty) * child.conversion_factor)
+			.where(
+				(table.docstatus == 1)
+				& (child.item_code == item_code)
+				& (child.warehouse == warehouse)
+				& (child.qty > child.received_qty)
+				& (table.status != "Completed")
+			)
+		)
+
+		query = query.run()
+
+		return flt(query[0][0]) if query else 0
 
 	def update_reserved_qty_for_subcontracting(self):
 		for item in self.supplied_items:
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
index 22fdc13..3557858 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
@@ -6,6 +6,7 @@
 
 import frappe
 from frappe.tests.utils import FrappeTestCase
+from frappe.utils import flt
 
 from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order
 from erpnext.controllers.subcontracting_controller import (
@@ -566,6 +567,67 @@
 		self.assertEqual(sco.status, "Closed")
 		self.assertEqual(sco.supplied_items[0].returned_qty, 5)
 
+	def test_ordered_qty_for_subcontracting_order(self):
+		service_items = [
+			{
+				"warehouse": "_Test Warehouse - _TC",
+				"item_code": "Subcontracted Service Item 8",
+				"qty": 10,
+				"rate": 100,
+				"fg_item": "Subcontracted Item SA8",
+				"fg_item_qty": 10,
+			},
+		]
+
+		ordered_qty = frappe.db.get_value(
+			"Bin",
+			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
+			fieldname="ordered_qty",
+		)
+		ordered_qty = flt(ordered_qty)
+
+		sco = get_subcontracting_order(service_items=service_items)
+		sco.reload()
+
+		new_ordered_qty = frappe.db.get_value(
+			"Bin",
+			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
+			fieldname="ordered_qty",
+		)
+		new_ordered_qty = flt(new_ordered_qty)
+
+		self.assertEqual(ordered_qty + 10, new_ordered_qty)
+
+		for row in sco.supplied_items:
+			make_stock_entry(
+				target="_Test Warehouse 1 - _TC",
+				item_code=row.rm_item_code,
+				qty=row.required_qty,
+				basic_rate=100,
+			)
+
+		scr = make_subcontracting_receipt(sco.name)
+		scr.submit()
+
+		new_ordered_qty = frappe.db.get_value(
+			"Bin",
+			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
+			fieldname="ordered_qty",
+		)
+
+		self.assertEqual(ordered_qty, new_ordered_qty)
+
+		scr.reload()
+		scr.cancel()
+
+		new_ordered_qty = frappe.db.get_value(
+			"Bin",
+			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
+			fieldname="ordered_qty",
+		)
+
+		self.assertEqual(ordered_qty + 10, new_ordered_qty)
+
 
 def create_subcontracting_order(**args):
 	args = frappe._dict(args)
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 8d705aa..ae64cc6 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -95,12 +95,12 @@
 		)
 		self.update_status_updater_args()
 		self.update_prevdoc_status()
-		self.update_stock_ledger()
-		self.make_gl_entries_on_cancel()
-		self.repost_future_sle_and_gle()
 		self.delete_auto_created_batches()
 		self.set_consumed_qty_in_subcontract_order()
 		self.set_subcontracting_order_status()
+		self.update_stock_ledger()
+		self.make_gl_entries_on_cancel()
+		self.repost_future_sle_and_gle()
 		self.update_status()
 
 	def validate_items_qty(self):
diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.js b/erpnext/support/doctype/warranty_claim/warranty_claim.js
index 358768e..10cb37f 100644
--- a/erpnext/support/doctype/warranty_claim/warranty_claim.js
+++ b/erpnext/support/doctype/warranty_claim/warranty_claim.js
@@ -4,93 +4,67 @@
 frappe.provide("erpnext.support");
 
 frappe.ui.form.on("Warranty Claim", {
-	setup: function(frm) {
-		frm.set_query('contact_person', erpnext.queries.contact_query);
-		frm.set_query('customer_address', erpnext.queries.address_query);
-		frm.set_query('customer', erpnext.queries.customer);
+	setup: (frm) => {
+		frm.set_query("contact_person", erpnext.queries.contact_query);
+		frm.set_query("customer_address", erpnext.queries.address_query);
+		frm.set_query("customer", erpnext.queries.customer);
 
-		frm.add_fetch('serial_no', 'item_code', 'item_code');
-		frm.add_fetch('serial_no', 'item_name', 'item_name');
-		frm.add_fetch('serial_no', 'description', 'description');
-		frm.add_fetch('serial_no', 'maintenance_status', 'warranty_amc_status');
-		frm.add_fetch('serial_no', 'warranty_expiry_date', 'warranty_expiry_date');
-		frm.add_fetch('serial_no', 'amc_expiry_date', 'amc_expiry_date');
-		frm.add_fetch('serial_no', 'customer', 'customer');
-		frm.add_fetch('serial_no', 'customer_name', 'customer_name');
-		frm.add_fetch('item_code', 'item_name', 'item_name');
-		frm.add_fetch('item_code', 'description', 'description');
+		frm.set_query("serial_no", () => {
+			let filters = {
+				company: frm.doc.company,
+			};
+
+			if (frm.doc.item_code) {
+				filters["item_code"] = frm.doc.item_code;
+			}
+
+			return { filters: filters };
+		});
+
+		frm.set_query("item_code", () => {
+			return {
+				filters: {
+					disabled: 0,
+				},
+			};
+		});
 	},
-	onload: function(frm) {
-		if(!frm.doc.status) {
-			frm.set_value('status', 'Open');
+
+	onload: (frm) => {
+		if (!frm.doc.status) {
+			frm.set_value("status", "Open");
 		}
 	},
-	customer: function(frm) {
+
+	refresh: (frm) => {
+		frappe.dynamic_link = {
+			doc: frm.doc,
+			fieldname: "customer",
+			doctype: "Customer",
+		};
+
+		if (
+			!frm.doc.__islocal &&
+			["Open", "Work In Progress"].includes(frm.doc.status)
+		) {
+			frm.add_custom_button(__("Maintenance Visit"), () => {
+				frappe.model.open_mapped_doc({
+					method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit",
+					frm: frm,
+				});
+			});
+		}
+	},
+
+	customer: (frm) => {
 		erpnext.utils.get_party_details(frm);
 	},
-	customer_address: function(frm) {
+
+	customer_address: (frm) => {
 		erpnext.utils.get_address_display(frm);
 	},
-	contact_person: function(frm) {
+
+	contact_person: (frm) => {
 		erpnext.utils.get_contact_details(frm);
-	}
+	},
 });
-
-erpnext.support.WarrantyClaim = class WarrantyClaim extends frappe.ui.form.Controller {
-	refresh() {
-		frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'}
-
-		if(!cur_frm.doc.__islocal &&
-			(cur_frm.doc.status=='Open' || cur_frm.doc.status == 'Work In Progress')) {
-			cur_frm.add_custom_button(__('Maintenance Visit'),
-				this.make_maintenance_visit);
-		}
-	}
-
-	make_maintenance_visit() {
-		frappe.model.open_mapped_doc({
-			method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit",
-			frm: cur_frm
-		})
-	}
-};
-
-extend_cscript(cur_frm.cscript, new erpnext.support.WarrantyClaim({frm: cur_frm}));
-
-cur_frm.fields_dict['serial_no'].get_query = function(doc, cdt, cdn) {
-	var cond = [];
-	var filter = [
-		['Serial No', 'docstatus', '!=', 2]
-	];
-	if(doc.item_code) {
-		cond = ['Serial No', 'item_code', '=', doc.item_code];
-		filter.push(cond);
-	}
-	if(doc.customer) {
-		cond = ['Serial No', 'customer', '=', doc.customer];
-		filter.push(cond);
-	}
-	return{
-		filters:filter
-	}
-}
-
-cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) {
-	if(doc.serial_no) {
-		return{
-			doctype: "Serial No",
-			fields: "item_code",
-			filters:{
-				name: doc.serial_no
-			}
-		}
-	}
-	else{
-		return{
-			filters:[
-				['Item', 'docstatus', '!=', 2],
-				['Item', 'disabled', '=', 0]
-			]
-		}
-	}
-};
diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.json b/erpnext/support/doctype/warranty_claim/warranty_claim.json
index 01d9b01..9af2b46 100644
--- a/erpnext/support/doctype/warranty_claim/warranty_claim.json
+++ b/erpnext/support/doctype/warranty_claim/warranty_claim.json
@@ -92,7 +92,8 @@
    "fieldname": "serial_no",
    "fieldtype": "Link",
    "label": "Serial No",
-   "options": "Serial No"
+   "options": "Serial No",
+   "search_index": 1
   },
   {
    "fieldname": "customer",
@@ -128,6 +129,8 @@
    "options": "fa fa-ticket"
   },
   {
+   "fetch_from": "serial_no.item_code",
+   "fetch_if_empty": 1,
    "fieldname": "item_code",
    "fieldtype": "Link",
    "in_list_view": 1,
@@ -140,6 +143,7 @@
   },
   {
    "depends_on": "eval:doc.item_code",
+   "fetch_from": "item_code.item_name",
    "fieldname": "item_name",
    "fieldtype": "Data",
    "label": "Item Name",
@@ -149,6 +153,7 @@
   },
   {
    "depends_on": "eval:doc.item_code",
+   "fetch_from": "item_code.description",
    "fieldname": "description",
    "fieldtype": "Small Text",
    "label": "Description",
@@ -164,17 +169,24 @@
    "width": "50%"
   },
   {
+   "fetch_from": "serial_no.maintenance_status",
+   "fetch_if_empty": 1,
    "fieldname": "warranty_amc_status",
    "fieldtype": "Select",
    "label": "Warranty / AMC Status",
-   "options": "\nUnder Warranty\nOut of Warranty\nUnder AMC\nOut of AMC"
+   "options": "\nUnder Warranty\nOut of Warranty\nUnder AMC\nOut of AMC",
+   "search_index": 1
   },
   {
+   "fetch_from": "serial_no.warranty_expiry_date",
+   "fetch_if_empty": 1,
    "fieldname": "warranty_expiry_date",
    "fieldtype": "Date",
    "label": "Warranty Expiry Date"
   },
   {
+   "fetch_from": "serial_no.amc_expiry_date",
+   "fetch_if_empty": 1,
    "fieldname": "amc_expiry_date",
    "fieldtype": "Date",
    "label": "AMC Expiry Date"
@@ -225,6 +237,7 @@
   {
    "bold": 1,
    "depends_on": "customer",
+   "fetch_from": "customer.customer_name",
    "fieldname": "customer_name",
    "fieldtype": "Data",
    "in_global_search": 1,
@@ -366,7 +379,7 @@
  "icon": "fa fa-bug",
  "idx": 1,
  "links": [],
- "modified": "2023-06-03 16:17:07.694449",
+ "modified": "2023-11-28 17:30:35.676410",
  "modified_by": "Administrator",
  "module": "Support",
  "name": "Warranty Claim",
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 7eba35d..b083614 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -98,6 +98,7 @@
 				"Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
 			)
 
+		stop_actions = []
 		for ref_dt, ref_dn_field, ref_link_field in ref_details:
 			reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
 			reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
@@ -108,7 +109,7 @@
 					if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
 						if action == "Stop":
 							if role_allowed_to_override not in frappe.get_roles():
-								frappe.throw(
+								stop_actions.append(
 									_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
 										d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate
 									)
@@ -121,6 +122,8 @@
 								title=_("Warning"),
 								indicator="orange",
 							)
+		if stop_actions:
+			frappe.throw(stop_actions, as_list=True)
 
 	def get_reference_details(self, reference_names, reference_doctype):
 		return frappe._dict(
diff --git a/erpnext/accounts/doctype/unreconcile_payments/__init__.py b/erpnext/www/all-products/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/unreconcile_payments/__init__.py
copy to erpnext/www/all-products/__init__.py
diff --git a/erpnext/accounts/doctype/unreconcile_payments/__init__.py b/erpnext/www/shop-by-category/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/unreconcile_payments/__init__.py
copy to erpnext/www/shop-by-category/__init__.py