Merge branch 'develop' into uae_setup_cleaup
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 859146b..69749c9 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -2,6 +2,13 @@
 
 set -e
 
+# Check for merge conflicts before proceeding
+python -m compileall -f "${GITHUB_WORKSPACE}"
+if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
+    then echo "Found merge conflicts"
+    exit 1
+fi
+
 cd ~ || exit
 
 sudo apt-get install redis-server libcups2-dev
diff --git a/.github/try-on-f-cloud-button.svg b/.github/try-on-f-cloud-button.svg
index fe0bb2c..2700927 100644
--- a/.github/try-on-f-cloud-button.svg
+++ b/.github/try-on-f-cloud-button.svg
@@ -1,4 +1,4 @@
-<svg width="201" height="60" viewBox="0 0 201 60" fill="none" xmlns="http://www.w3.org/2000/svg">
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="4 2 193 52">
 <g filter="url(#filter0_dd)">
 <rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/>
 <path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>
diff --git a/README.md b/README.md
index c26660c..cea3472 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
 <div align="center">
-    <img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128">
+    <a href="https://erpnext.com">
+        <img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128">
+    </a>
     <h2>ERPNext</h2>
     <p align="center">
         <p>ERP made simple</p>
@@ -32,40 +34,39 @@
 1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext)
 1. [And More](https://erpnext.com/docs/user/manual/en/)
 
-ERPNext requires MariaDB.
-
 ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript.
 
-- [User Guide](https://erpnext.com/docs/user)
-- [Discussion Forum](https://discuss.erpnext.com/)
+## Installation
 
----
-
-<div align="center">
-    <a href="https://frappecloud.com/deploy?apps=frappe,erpnext&source=erpnext_readme">
+<div align="center" style="max-height: 40px;">
+    <a href="https://frappecloud.com/erpnext/signup">
         <img src=".github/try-on-f-cloud-button.svg" height="40">
     </a>
+    <a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml">
+      <img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD" height="37"/>
+    </a>
 </div>
 
+> Login for the PWD site: (username: Administrator, password: admin)
+
 ### Containerized Installation
 
 Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.
 
-### Full Install
+### Manual Install
 
 The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details.
 
 New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt).
 
----
 
-## License
+## Learning and community
 
-GNU/General Public License (see [license.txt](license.txt))
+1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
+2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
+3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
+4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users.
 
-The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
-
----
 
 ## Contributing
 
@@ -73,49 +74,14 @@
 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)
-1. [Chart of Accounts](https://charts.erpnext.com)
 
----
 
-## Learning
+## License
 
-1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
+GNU/General Public License (see [license.txt](license.txt))
 
----
+The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
 
-## Logo and Trademark
+## Logo and Trademark Policy
 
-The brand name ERPNext and the logo are trademarks of Frappe Technologies Pvt. Ltd.
-
-### Introduction
-
-Frappe Technologies Pvt. Ltd. (Frappe) owns and oversees the trademarks for the ERPNext name and logos. We have developed this trademark usage policy with the following goals in mind:
-
-- We’d like to make it easy for anyone to use the ERPNext name or logo for community-oriented efforts that help spread and improve ERPNext.
-- We’d like to make it clear how ERPNext-related businesses and projects can (and cannot) use the ERPNext name and logo.
-- We’d like to make it hard for anyone to use the ERPNext name and logo to unfairly profit from, trick or confuse people who are looking for official ERPNext resources.
-
-### Frappe Trademark Usage Policy
-
-Permission from Frappe is required to use the ERPNext name or logo as part of any project, product, service, domain or company name.
-
-We will grant permission to use the ERPNext name and logo for projects that meet the following criteria:
-
-- The primary purpose of your project is to promote the spread and improvement of the ERPNext software.
-- Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business).
-Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed.
-- If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name.
-
-Use of the ERPNext name and logo is additionally allowed in the following situations:
-
-All other ERPNext-related businesses or projects can use the ERPNext name and logo to refer to and explain their services, but they cannot use them as part of a product, project, service, domain, or company name and they cannot use them in any way that suggests an affiliation with or endorsement by ERPNext or Frappe Technologies or the ERPNext open source project. For example, a consulting company can describe its business as “123 Web Services, offering ERPNext consulting for small businesses,” but cannot call its business “The ERPNext Consulting Company.”
-
-Similarly, it’s OK to use the ERPNext logo as part of a page that describes your products or services, but it is not OK to use it as part of your company or product logo or branding itself. Under no circumstances is it permitted to use ERPNext as part of a top-level domain name.
-
-We do not allow the use of the trademark in advertising, including AdSense/AdWords.
-
-Please note that it is not the goal of this policy to limit commercial activity around ERPNext. We encourage ERPNext-based businesses, and we would love to see hundreds of them.
-
-When in doubt about your use of the ERPNext name or logo, please contact Frappe Technologies for clarification.
-
-(inspired by WordPress)
+Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).
diff --git a/TRADEMARK_POLICY.md b/TRADEMARK_POLICY.md
new file mode 100644
index 0000000..244c747
--- /dev/null
+++ b/TRADEMARK_POLICY.md
@@ -0,0 +1,36 @@
+## Logo and Trademark Policy
+
+The brand name ERPNext and the logo are trademarks of Frappe Technologies Pvt. Ltd.
+
+### Introduction
+
+Frappe Technologies Pvt. Ltd. (Frappe) owns and oversees the trademarks for the ERPNext name and logos. We have developed this trademark usage policy with the following goals in mind:
+
+- We’d like to make it easy for anyone to use the ERPNext name or logo for community-oriented efforts that help spread and improve ERPNext.
+- We’d like to make it clear how ERPNext-related businesses and projects can (and cannot) use the ERPNext name and logo.
+- We’d like to make it hard for anyone to use the ERPNext name and logo to unfairly profit from, trick or confuse people who are looking for official ERPNext resources.
+
+### Frappe Trademark Usage Policy
+
+Permission from Frappe is required to use the ERPNext name or logo as part of any project, product, service, domain or company name.
+
+We will grant permission to use the ERPNext name and logo for projects that meet the following criteria:
+
+- The primary purpose of your project is to promote the spread and improvement of the ERPNext software.
+- Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business).
+Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed.
+- If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name.
+
+Use of the ERPNext name and logo is additionally allowed in the following situations:
+
+All other ERPNext-related businesses or projects can use the ERPNext name and logo to refer to and explain their services, but they cannot use them as part of a product, project, service, domain, or company name and they cannot use them in any way that suggests an affiliation with or endorsement by ERPNext or Frappe Technologies or the ERPNext open source project. For example, a consulting company can describe its business as “123 Web Services, offering ERPNext consulting for small businesses,” but cannot call its business “The ERPNext Consulting Company.”
+
+Similarly, it’s OK to use the ERPNext logo as part of a page that describes your products or services, but it is not OK to use it as part of your company or product logo or branding itself. Under no circumstances is it permitted to use ERPNext as part of a top-level domain name.
+
+We do not allow the use of the trademark in advertising, including AdSense/AdWords.
+
+Please note that it is not the goal of this policy to limit commercial activity around ERPNext. We encourage ERPNext-based businesses, and we would love to see hundreds of them.
+
+When in doubt about your use of the ERPNext name or logo, please contact Frappe Technologies for clarification.
+
+(inspired by WordPress)
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index 4453783..3f1998a 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -99,7 +99,7 @@
 			if doctype == "Budget":
 				add_dimension_to_budget_doctype(df.copy(), doc)
 			else:
-				create_custom_field(doctype, df)
+				create_custom_field(doctype, df, ignore_validate=True)
 
 		count += 1
 
@@ -115,7 +115,7 @@
 		}
 	)
 
-	create_custom_field("Budget", df)
+	create_custom_field("Budget", df, ignore_validate=True)
 
 	property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options")
 
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 9a35a24..417611f 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -18,7 +18,6 @@
   "automatically_fetch_payment_terms",
   "column_break_17",
   "enable_common_party_accounting",
-  "enable_discount_accounting",
   "report_setting_section",
   "use_custom_cash_flow",
   "deferred_accounting_settings_section",
@@ -274,13 +273,6 @@
   },
   {
    "default": "0",
-   "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
-   "fieldname": "enable_discount_accounting",
-   "fieldtype": "Check",
-   "label": "Enable Discount Accounting"
-  },
-  {
-   "default": "0",
    "description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
    "fieldname": "enable_common_party_accounting",
    "fieldtype": "Check",
@@ -354,7 +346,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2022-02-04 12:32:36.805652",
+ "modified": "2022-04-08 14:45:06.796418",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index 8354981..3b125a2 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -28,7 +28,6 @@
 
 		self.validate_stale_days()
 		self.enable_payment_schedule_in_print()
-		self.toggle_discount_accounting_fields()
 		self.validate_pending_reposts()
 
 	def validate_stale_days(self):
@@ -52,74 +51,6 @@
 				validate_fields_for_doctype=False,
 			)
 
-	def toggle_discount_accounting_fields(self):
-		enable_discount_accounting = cint(self.enable_discount_accounting)
-
-		for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]:
-			make_property_setter(
-				doctype,
-				"discount_account",
-				"hidden",
-				not (enable_discount_accounting),
-				"Check",
-				validate_fields_for_doctype=False,
-			)
-			if enable_discount_accounting:
-				make_property_setter(
-					doctype,
-					"discount_account",
-					"mandatory_depends_on",
-					"eval: doc.discount_amount",
-					"Code",
-					validate_fields_for_doctype=False,
-				)
-			else:
-				make_property_setter(
-					doctype,
-					"discount_account",
-					"mandatory_depends_on",
-					"",
-					"Code",
-					validate_fields_for_doctype=False,
-				)
-
-		for doctype in ["Sales Invoice", "Purchase Invoice"]:
-			make_property_setter(
-				doctype,
-				"additional_discount_account",
-				"hidden",
-				not (enable_discount_accounting),
-				"Check",
-				validate_fields_for_doctype=False,
-			)
-			if enable_discount_accounting:
-				make_property_setter(
-					doctype,
-					"additional_discount_account",
-					"mandatory_depends_on",
-					"eval: doc.discount_amount",
-					"Code",
-					validate_fields_for_doctype=False,
-				)
-			else:
-				make_property_setter(
-					doctype,
-					"additional_discount_account",
-					"mandatory_depends_on",
-					"",
-					"Code",
-					validate_fields_for_doctype=False,
-				)
-
-		make_property_setter(
-			"Item",
-			"default_discount_account",
-			"hidden",
-			not (enable_discount_accounting),
-			"Check",
-			validate_fields_for_doctype=False,
-		)
-
 	def validate_pending_reposts(self):
 		if self.acc_frozen_upto:
 			check_pending_reposting(self.acc_frozen_upto)
diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
index 96779d7..0f617b5 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -5,7 +5,10 @@
 import frappe
 from frappe import _, msgprint
 from frappe.model.document import Document
-from frappe.utils import flt, fmt_money, getdate, nowdate
+from frappe.query_builder.custom import ConstantColumn
+from frappe.utils import flt, fmt_money, getdate
+
+import erpnext
 
 form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"}
 
@@ -76,6 +79,52 @@
 			as_dict=1,
 		)
 
+		loan_disbursement = frappe.qb.DocType("Loan Disbursement")
+
+		loan_disbursements = (
+			frappe.qb.from_(loan_disbursement)
+			.select(
+				ConstantColumn("Loan Disbursement").as_("payment_document"),
+				loan_disbursement.name.as_("payment_entry"),
+				loan_disbursement.disbursed_amount.as_("credit"),
+				ConstantColumn(0).as_("debit"),
+				loan_disbursement.reference_number.as_("cheque_number"),
+				loan_disbursement.reference_date.as_("cheque_date"),
+				loan_disbursement.disbursement_date.as_("posting_date"),
+				loan_disbursement.applicant.as_("against_account"),
+			)
+			.where(loan_disbursement.docstatus == 1)
+			.where(loan_disbursement.disbursement_date >= self.from_date)
+			.where(loan_disbursement.disbursement_date <= self.to_date)
+			.where(loan_disbursement.clearance_date.isnull())
+			.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
+			.orderby(loan_disbursement.disbursement_date)
+			.orderby(loan_disbursement.name, frappe.qb.desc)
+		).run(as_dict=1)
+
+		loan_repayment = frappe.qb.DocType("Loan Repayment")
+
+		loan_repayments = (
+			frappe.qb.from_(loan_repayment)
+			.select(
+				ConstantColumn("Loan Repayment").as_("payment_document"),
+				loan_repayment.name.as_("payment_entry"),
+				loan_repayment.amount_paid.as_("debit"),
+				ConstantColumn(0).as_("credit"),
+				loan_repayment.reference_number.as_("cheque_number"),
+				loan_repayment.reference_date.as_("cheque_date"),
+				loan_repayment.applicant.as_("against_account"),
+				loan_repayment.posting_date,
+			)
+			.where(loan_repayment.docstatus == 1)
+			.where(loan_repayment.clearance_date.isnull())
+			.where(loan_repayment.posting_date >= self.from_date)
+			.where(loan_repayment.posting_date <= self.to_date)
+			.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
+			.orderby(loan_repayment.posting_date)
+			.orderby(loan_repayment.name, frappe.qb.desc)
+		).run(as_dict=1)
+
 		pos_sales_invoices, pos_purchase_invoices = [], []
 		if self.include_pos_transactions:
 			pos_sales_invoices = frappe.db.sql(
@@ -114,20 +163,29 @@
 
 		entries = sorted(
 			list(payment_entries)
-			+ list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)),
-			key=lambda k: k["posting_date"] or getdate(nowdate()),
+			+ list(journal_entries)
+			+ list(pos_sales_invoices)
+			+ list(pos_purchase_invoices)
+			+ list(loan_disbursements)
+			+ list(loan_repayments),
+			key=lambda k: getdate(k["posting_date"]),
 		)
 
 		self.set("payment_entries", [])
 		self.total_amount = 0.0
+		default_currency = erpnext.get_default_currency()
 
 		for d in entries:
 			row = self.append("payment_entries", {})
 
 			amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0))
 
+			if not d.get("account_currency"):
+				d.account_currency = default_currency
+
 			formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
 			d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
+			d.posting_date = getdate(d.posting_date)
 
 			d.pop("credit")
 			d.pop("debit")
diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py
index 706fbbe..c1e55f6 100644
--- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py
@@ -1,9 +1,96 @@
 # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
 
-# import frappe
 import unittest
 
+import frappe
+from frappe.utils import add_months, getdate
+
+from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+from erpnext.loan_management.doctype.loan.test_loan import (
+	create_loan,
+	create_loan_accounts,
+	create_loan_type,
+	create_repayment_entry,
+	make_loan_disbursement_entry,
+)
+
 
 class TestBankClearance(unittest.TestCase):
-	pass
+	@classmethod
+	def setUpClass(cls):
+		make_bank_account()
+		create_loan_accounts()
+		create_loan_masters()
+		add_transactions()
+
+	# Basic test case to test if bank clearance tool doesn't break
+	# Detailed test can be added later
+	def test_bank_clearance(self):
+		bank_clearance = frappe.get_doc("Bank Clearance")
+		bank_clearance.account = "_Test Bank Clearance - _TC"
+		bank_clearance.from_date = add_months(getdate(), -1)
+		bank_clearance.to_date = getdate()
+		bank_clearance.get_payment_entries()
+		self.assertEqual(len(bank_clearance.payment_entries), 3)
+
+
+def make_bank_account():
+	if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"):
+		frappe.get_doc(
+			{
+				"doctype": "Account",
+				"account_type": "Bank",
+				"account_name": "_Test Bank Clearance",
+				"company": "_Test Company",
+				"parent_account": "Bank Accounts - _TC",
+			}
+		).insert()
+
+
+def create_loan_masters():
+	create_loan_type(
+		"Clearance Loan",
+		2000000,
+		13.5,
+		25,
+		0,
+		5,
+		"Cash",
+		"_Test Bank Clearance - _TC",
+		"_Test Bank Clearance - _TC",
+		"Loan Account - _TC",
+		"Interest Income Account - _TC",
+		"Penalty Income Account - _TC",
+	)
+
+
+def add_transactions():
+	make_payment_entry()
+	make_loan()
+
+
+def make_loan():
+	loan = create_loan(
+		"_Test Customer",
+		"Clearance Loan",
+		280000,
+		"Repay Over Number of Periods",
+		20,
+		applicant_type="Customer",
+	)
+	loan.submit()
+	make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
+	repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
+	repayment_entry.save()
+	repayment_entry.submit()
+
+
+def make_payment_entry():
+	pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690)
+	pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
+	pe.reference_no = "Conrad Oct 18"
+	pe.reference_date = "2018-10-24"
+	pe.insert()
+	pe.submit()
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index 9d4d76b..2d9ae93 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -114,10 +114,13 @@
 				)
 				or {}
 			)
+
+			default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency")
+
 			if company_details:
 				invoice.update(
 					{
-						"currency": company_details.get("default_currency"),
+						"currency": default_currency or company_details.get("default_currency"),
 						"letter_head": company_details.get("default_letter_head"),
 					}
 				)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index a5f9e24..a1d86e2 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -669,7 +669,7 @@
 		exchange_rate_map, net_rate_map = get_purchase_document_details(self)
 
 		enable_discount_accounting = cint(
-			frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
+			frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
 		)
 		provisional_accounting_for_non_stock_items = cint(
 			frappe.db.get_value(
@@ -1159,7 +1159,7 @@
 		# tax table gl entries
 		valuation_tax = {}
 		enable_discount_accounting = cint(
-			frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
+			frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
 		)
 
 		for tax in self.get("taxes"):
@@ -1252,7 +1252,7 @@
 	def enable_discount_accounting(self):
 		if not hasattr(self, "_enable_discount_accounting"):
 			self._enable_discount_accounting = cint(
-				frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
+				frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
 			)
 
 		return self._enable_discount_accounting
@@ -1369,7 +1369,9 @@
 		if (
 			not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
 		):
-			round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company)
+			round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+				self.company, "Purchase Invoice", self.name
+			)
 
 			gl_entries.append(
 				self.get_gl_dict(
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 59bd637..30d26ac 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -5,6 +5,7 @@
 import unittest
 
 import frappe
+from frappe.tests.utils import change_settings
 from frappe.utils import add_days, cint, flt, getdate, nowdate, today
 
 import erpnext
@@ -336,8 +337,8 @@
 
 		self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
 
+	@change_settings("Buying Settings", {"enable_discount_accounting": 1})
 	def test_purchase_invoice_with_discount_accounting_enabled(self):
-		enable_discount_accounting()
 
 		discount_account = create_account(
 			account_name="Discount Account",
@@ -353,10 +354,10 @@
 		]
 
 		check_gl_entries(self, pi.name, expected_gle, nowdate())
-		enable_discount_accounting(enable=0)
 
+	@change_settings("Buying Settings", {"enable_discount_accounting": 1})
 	def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self):
-		enable_discount_accounting()
+
 		additional_discount_account = create_account(
 			account_name="Discount Account",
 			parent_account="Indirect Expenses - _TC",
@@ -1588,12 +1589,6 @@
 	accounts_settings.save()
 
 
-def enable_discount_accounting(enable=1):
-	accounts_settings = frappe.get_doc("Accounts Settings")
-	accounts_settings.enable_discount_accounting = enable
-	accounts_settings.save()
-
-
 def make_purchase_invoice(**args):
 	pi = frappe.new_doc("Purchase Invoice")
 	args = frappe._dict(args)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 1efd3dc..f0880c1 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1051,7 +1051,7 @@
 
 	def make_tax_gl_entries(self, gl_entries):
 		enable_discount_accounting = cint(
-			frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
+			frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
 		)
 
 		for tax in self.get("taxes"):
@@ -1097,7 +1097,7 @@
 	def make_item_gl_entries(self, gl_entries):
 		# income account gl entries
 		enable_discount_accounting = cint(
-			frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
+			frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
 		)
 
 		for item in self.get("items"):
@@ -1276,7 +1276,7 @@
 	def enable_discount_accounting(self):
 		if not hasattr(self, "_enable_discount_accounting"):
 			self._enable_discount_accounting = cint(
-				frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
+				frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
 			)
 
 		return self._enable_discount_accounting
@@ -1466,7 +1466,9 @@
 			and self.base_rounding_adjustment
 			and not self.is_internal_transfer()
 		):
-			round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company)
+			round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+				self.company, "Sales Invoice", self.name
+			)
 
 			gl_entries.append(
 				self.get_gl_dict(
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index caa70d0..f2a696d 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -7,6 +7,7 @@
 import frappe
 from frappe.model.dynamic_links import get_dynamic_link_map
 from frappe.model.naming import make_autoname
+from frappe.tests.utils import change_settings
 from frappe.utils import add_days, flt, getdate, nowdate
 
 import erpnext
@@ -1977,6 +1978,13 @@
 			self.assertEqual(expected_values[gle.account][2], gle.credit)
 
 	def test_rounding_adjustment_3(self):
+		from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
+			create_dimension,
+			disable_dimension,
+		)
+
+		create_dimension()
+
 		si = create_sales_invoice(do_not_save=True)
 		si.items = []
 		for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
@@ -2004,6 +2012,10 @@
 					"included_in_print_rate": 1,
 				},
 			)
+
+		si.cost_center = "_Test Cost Center 2 - _TC"
+		si.location = "Block 1"
+
 		si.save()
 		si.submit()
 		self.assertEqual(si.net_total, 4007.16)
@@ -2039,6 +2051,18 @@
 
 		self.assertEqual(debit_credit_diff, 0)
 
+		round_off_gle = frappe.db.get_value(
+			"GL Entry",
+			{"voucher_type": "Sales Invoice", "voucher_no": si.name, "account": "Round Off - _TC"},
+			["cost_center", "location"],
+			as_dict=1,
+		)
+
+		self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC")
+		self.assertEqual(round_off_gle.location, "Block 1")
+
+		disable_dimension()
+
 	def test_sales_invoice_with_shipping_rule(self):
 		from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
 
@@ -2684,12 +2708,8 @@
 			sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC"
 		)
 
+	@change_settings("Selling Settings", {"enable_discount_accounting": 1})
 	def test_sales_invoice_with_discount_accounting_enabled(self):
-		from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
-			enable_discount_accounting,
-		)
-
-		enable_discount_accounting()
 
 		discount_account = create_account(
 			account_name="Discount Account",
@@ -2705,14 +2725,10 @@
 		]
 
 		check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
-		enable_discount_accounting(enable=0)
 
+	@change_settings("Selling Settings", {"enable_discount_accounting": 1})
 	def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
-		from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
-			enable_discount_accounting,
-		)
 
-		enable_discount_accounting()
 		additional_discount_account = create_account(
 			account_name="Discount Account",
 			parent_account="Indirect Expenses - _TC",
@@ -2743,7 +2759,6 @@
 		]
 
 		check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
-		enable_discount_accounting(enable=0)
 
 	def test_asset_depreciation_on_sale_with_pro_rata(self):
 		"""
diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py
index 27b78e9..5bfca96 100644
--- a/erpnext/accounts/doctype/tax_rule/tax_rule.py
+++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py
@@ -163,10 +163,15 @@
 def get_tax_template(posting_date, args):
 	"""Get matching tax rule"""
 	args = frappe._dict(args)
+	from_date = to_date = posting_date
+	if not posting_date:
+		from_date = "1900-01-01"
+		to_date = "4000-01-01"
+
 	conditions = [
 		"""(from_date is null or from_date <= '{0}')
-		and (to_date is null or to_date >= '{0}')""".format(
-			posting_date
+		and (to_date is null or to_date >= '{1}')""".format(
+			from_date, to_date
 		)
 	]
 
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index f52e517..89034eb 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -355,7 +355,7 @@
 
 def make_round_off_gle(gl_map, debit_credit_diff, precision):
 	round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
-		gl_map[0].company
+		gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
 	)
 	round_off_account_exists = False
 	round_off_gle = frappe._dict()
@@ -392,14 +392,43 @@
 		}
 	)
 
+	update_accounting_dimensions(round_off_gle)
+
 	if not round_off_account_exists:
 		gl_map.append(round_off_gle)
 
 
-def get_round_off_account_and_cost_center(company):
+def update_accounting_dimensions(round_off_gle):
+	dimensions = get_accounting_dimensions()
+	meta = frappe.get_meta(round_off_gle["voucher_type"])
+	has_all_dimensions = True
+
+	for dimension in dimensions:
+		if not meta.has_field(dimension):
+			has_all_dimensions = False
+
+	if dimensions and has_all_dimensions:
+		dimension_values = frappe.db.get_value(
+			round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1
+		)
+
+		for dimension in dimensions:
+			round_off_gle[dimension] = dimension_values.get(dimension)
+
+
+def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
 	round_off_account, round_off_cost_center = frappe.get_cached_value(
 		"Company", company, ["round_off_account", "round_off_cost_center"]
 	) or [None, None]
+
+	meta = frappe.get_meta(voucher_type)
+
+	# Give first preference to parent cost center for round off GLE
+	if meta.has_field("cost_center"):
+		parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
+		if parent_cost_center:
+			round_off_cost_center = parent_cost_center
+
 	if not round_off_account:
 		frappe.throw(_("Please mention Round Off Account in Company"))
 
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index b0b3049..db741d9 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -831,9 +831,9 @@
 		"where "
 		"dl.link_doctype=%s "
 		"and dl.link_name=%s "
-		'and dl.parenttype="Address" '
+		"and dl.parenttype='Address' "
 		"and ifnull(ta.disabled, 0) = 0 and"
-		'(ta.address_type="Shipping" or ta.is_shipping_address=1) '
+		"(ta.address_type='Shipping' or ta.is_shipping_address=1) "
 		"order by ta.is_shipping_address desc, ta.address_type desc limit 1",
 		(doctype, name),
 	)
@@ -881,11 +881,11 @@
 		"""
 			SELECT dl.parent, c.is_primary_contact, c.is_billing_contact
 			FROM `tabDynamic Link` dl
-			INNER JOIN tabContact c ON c.name = dl.parent
+			INNER JOIN `tabContact` c ON c.name = dl.parent
 			WHERE
 				dl.link_doctype=%s AND
 				dl.link_name=%s AND
-				dl.parenttype = "Contact"
+				dl.parenttype = 'Contact'
 			ORDER BY is_primary_contact DESC, is_billing_contact DESC
 		""",
 		(doctype, name),
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 50321ba..89a9448 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -20,6 +20,7 @@
   "maintain_same_rate",
   "allow_multiple_items",
   "bill_for_rejected_quantity_in_purchase_invoice",
+  "enable_discount_accounting",
   "subcontract",
   "backflush_raw_materials_of_subcontract_based_on",
   "column_break_11",
@@ -133,6 +134,13 @@
   {
    "fieldname": "column_break_12",
    "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
+   "fieldname": "enable_discount_accounting",
+   "fieldtype": "Check",
+   "label": "Enable Discount Accounting for Buying"
   }
  ],
  "icon": "fa fa-cog",
@@ -140,7 +148,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2022-01-27 17:57:58.367048",
+ "modified": "2022-04-14 15:56:42.340223",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Buying Settings",
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py
index 5507254..c52b59e 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.py
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.py
@@ -5,10 +5,15 @@
 
 
 import frappe
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter
 from frappe.model.document import Document
+from frappe.utils import cint
 
 
 class BuyingSettings(Document):
+	def on_update(self):
+		self.toggle_discount_accounting_fields()
+
 	def validate(self):
 		for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]:
 			frappe.db.set_default(key, self.get(key, ""))
@@ -21,3 +26,60 @@
 			self.get("supp_master_name") == "Naming Series",
 			hide_name_field=False,
 		)
+
+	def toggle_discount_accounting_fields(self):
+		enable_discount_accounting = cint(self.enable_discount_accounting)
+
+		make_property_setter(
+			"Purchase Invoice Item",
+			"discount_account",
+			"hidden",
+			not (enable_discount_accounting),
+			"Check",
+			validate_fields_for_doctype=False,
+		)
+		if enable_discount_accounting:
+			make_property_setter(
+				"Purchase Invoice Item",
+				"discount_account",
+				"mandatory_depends_on",
+				"eval: doc.discount_amount",
+				"Code",
+				validate_fields_for_doctype=False,
+			)
+		else:
+			make_property_setter(
+				"Purchase Invoice Item",
+				"discount_account",
+				"mandatory_depends_on",
+				"",
+				"Code",
+				validate_fields_for_doctype=False,
+			)
+
+		make_property_setter(
+			"Purchase Invoice",
+			"additional_discount_account",
+			"hidden",
+			not (enable_discount_accounting),
+			"Check",
+			validate_fields_for_doctype=False,
+		)
+		if enable_discount_accounting:
+			make_property_setter(
+				"Purchase Invoice",
+				"additional_discount_account",
+				"mandatory_depends_on",
+				"eval: doc.discount_amount",
+				"Code",
+				validate_fields_for_doctype=False,
+			)
+		else:
+			make_property_setter(
+				"Purchase Invoice",
+				"additional_discount_account",
+				"mandatory_depends_on",
+				"",
+				"Code",
+				validate_fields_for_doctype=False,
+			)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index f20df09..78645e0 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1079,9 +1079,14 @@
 		return amount, base_amount
 
 	def make_discount_gl_entries(self, gl_entries):
-		enable_discount_accounting = cint(
-			frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
-		)
+		if self.doctype == "Purchase Invoice":
+			enable_discount_accounting = cint(
+				frappe.db.get_single_value("Buying Settings", "enable_discount_accounting")
+			)
+		elif self.doctype == "Sales Invoice":
+			enable_discount_accounting = cint(
+				frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
+			)
 
 		if enable_discount_accounting:
 			if self.doctype == "Purchase Invoice":
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 96c730c..19b4d68 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -54,11 +54,11 @@
 			self.calculate_totals()
 
 	def map_fields(self):
-		for field in self.meta.fields:
-			if not self.get(field.fieldname):
+		for field in self.meta.get_valid_columns():
+			if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field):
 				try:
-					value = frappe.db.get_value(self.opportunity_from, self.party_name, field.fieldname)
-					frappe.db.set(self, field.fieldname, value)
+					value = frappe.db.get_value(self.opportunity_from, self.party_name, field)
+					frappe.db.set(self, field, value)
 				except Exception:
 					continue
 
diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
index 64eb019..e07b5e5 100644
--- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py
+++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
@@ -5,7 +5,7 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe.utils import cint, get_datetime
+from frappe.utils import cint, get_datetime, get_link_to_form
 
 from erpnext.hr.doctype.attendance.attendance import (
 	get_duplicate_attendance_record,
@@ -130,14 +130,11 @@
 	"""
 	log_names = [x.name for x in logs]
 	employee = logs[0].employee
+
 	if attendance_status == "Skip":
-		frappe.db.sql(
-			"""update `tabEmployee Checkin`
-			set skip_auto_attendance = %s
-			where name in %s""",
-			("1", log_names),
-		)
+		skip_attendance_in_checkins(log_names)
 		return None
+
 	elif attendance_status in ("Present", "Absent", "Half Day"):
 		employee_doc = frappe.get_doc("Employee", employee)
 		duplicate = get_duplicate_attendance_record(employee, attendance_date, shift)
@@ -159,6 +156,12 @@
 			}
 			attendance = frappe.get_doc(doc_dict).insert()
 			attendance.submit()
+
+			if attendance_status == "Absent":
+				attendance.add_comment(
+					text=_("Employee was marked Absent for not meeting the working hours threshold.")
+				)
+
 			frappe.db.sql(
 				"""update `tabEmployee Checkin`
 				set attendance = %s
@@ -167,13 +170,10 @@
 			)
 			return attendance
 		else:
-			frappe.db.sql(
-				"""update `tabEmployee Checkin`
-				set skip_auto_attendance = %s
-				where name in %s""",
-				("1", log_names),
-			)
+			skip_attendance_in_checkins(log_names)
+			add_comment_in_checkins(log_names, duplicate, overlapping)
 			return None
+
 	else:
 		frappe.throw(_("{} is an invalid Attendance Status.").format(attendance_status))
 
@@ -241,3 +241,34 @@
 
 def find_index_in_dict(dict_list, key, value):
 	return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None)
+
+
+def add_comment_in_checkins(log_names, duplicate, overlapping):
+	if duplicate:
+		text = _("Auto Attendance skipped due to duplicate attendance record: {}").format(
+			get_link_to_form("Attendance", duplicate[0].name)
+		)
+	else:
+		text = _("Auto Attendance skipped due to overlapping attendance record: {}").format(
+			get_link_to_form("Attendance", overlapping.name)
+		)
+
+	for name in log_names:
+		frappe.get_doc(
+			{
+				"doctype": "Comment",
+				"comment_type": "Comment",
+				"reference_doctype": "Employee Checkin",
+				"reference_name": name,
+				"content": text,
+			}
+		).insert(ignore_permissions=True)
+
+
+def skip_attendance_in_checkins(log_names):
+	EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
+	(
+		frappe.qb.update(EmployeeCheckin)
+		.set("skip_auto_attendance", 1)
+		.where(EmployeeCheckin.name.isin(log_names))
+	).run()
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
index dde52d7..a1d39d4 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
@@ -18,6 +18,7 @@
 	def setUp(self):
 		frappe.db.delete("Leave Period")
 		frappe.db.delete("Leave Allocation")
+		frappe.db.delete("Leave Ledger Entry")
 
 		emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company")
 		self.employee = frappe.get_doc("Employee", emp_id)
diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py
index 5e214cf..a61bb9e 100644
--- a/erpnext/hr/doctype/shift_type/shift_type.py
+++ b/erpnext/hr/doctype/shift_type/shift_type.py
@@ -134,7 +134,17 @@
 			shift_details = get_employee_shift(employee, timestamp, True)
 
 			if shift_details and shift_details.shift_type.name == self.name:
-				mark_attendance(employee, date, "Absent", self.name)
+				attendance = mark_attendance(employee, date, "Absent", self.name)
+				if attendance:
+					frappe.get_doc(
+						{
+							"doctype": "Comment",
+							"comment_type": "Comment",
+							"reference_doctype": "Attendance",
+							"reference_name": attendance,
+							"content": frappe._("Employee was marked Absent due to missing Employee Checkins."),
+						}
+					).insert(ignore_permissions=True)
 
 	def get_start_and_end_dates(self, employee):
 		"""Returns start and end dates for checking attendance and marking absent
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index fefb2e5..220ce1d 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -753,7 +753,7 @@
 				bom_item.include_item_in_manufacturing,
 				bom_item.sourced_by_supplier,
 				bom_item.stock_qty / ifnull(bom.quantity, 1) AS qty_consumed_per_unit
-			FROM `tabBOM Explosion Item` bom_item, tabBOM bom
+			FROM `tabBOM Explosion Item` bom_item, `tabBOM` bom
 			WHERE
 				bom_item.parent = bom.name
 				AND bom.name = %s
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d163ca5..d6c44cb 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -365,4 +365,5 @@
 erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
 erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
 erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
+erpnext.patches.v14_0.discount_accounting_separation
 erpnext.patches.v14_0.delete_employee_transfer_property_doctype
diff --git a/erpnext/patches/v14_0/discount_accounting_separation.py b/erpnext/patches/v14_0/discount_accounting_separation.py
new file mode 100644
index 0000000..fd49805
--- /dev/null
+++ b/erpnext/patches/v14_0/discount_accounting_separation.py
@@ -0,0 +1,9 @@
+import frappe
+
+
+def execute():
+	doc = frappe.get_doc("Accounts Settings")
+	discount_account = doc.enable_discount_accounting
+	if discount_account:
+		for doctype in ["Buying Settings", "Selling Settings"]:
+			frappe.db.set_value(doctype, doctype, "enable_discount_accounting", 1, update_modified=False)
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index a3abe86..0f73c5f 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -553,6 +553,7 @@
 		+ flt(value_details["CgstVal"])
 		+ flt(value_details["SgstVal"])
 		+ flt(value_details["IgstVal"])
+		+ flt(value_details["CesVal"])
 		+ flt(value_details["OthChrg"])
 		+ flt(value_details["RndOffAmt"])
 		- flt(value_details["Discount"])
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 446faaa..062c2ef 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -1219,7 +1219,7 @@
 		try:
 			doc = frappe.get_doc(d)
 			doc.flags.ignore_permissions = True
-			doc.insert()
+			doc.insert(ignore_if_duplicate=True)
 		except frappe.NameError:
 			frappe.clear_messages()
 		except frappe.DuplicateEntryError:
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 0b48f70..26c9996 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -152,7 +152,9 @@
 						}
 					}
 
-					this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create'));
+					if (flt(doc.per_picked, 6) < 100 && flt(doc.per_delivered, 6) < 100) {
+						this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create'));
+					}
 
 					const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1;
 					const order_is_maintenance = ["Maintenance"].indexOf(doc.order_type) !== -1;
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index fe2f14e..1d0432b 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -1520,6 +1520,7 @@
    "fieldname": "per_picked",
    "fieldtype": "Percent",
    "label": "% Picked",
+   "no_copy": 1,
    "read_only": 1
   }
  ],
@@ -1527,7 +1528,7 @@
  "idx": 105,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-03-15 21:38:31.437586",
+ "modified": "2022-04-21 08:16:48.316074",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index d3b4286..b463213 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -385,6 +385,16 @@
 		if tot_qty != 0:
 			self.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False)
 
+	def update_picking_status(self):
+		total_picked_qty = 0.0
+		total_qty = 0.0
+		for so_item in self.items:
+			total_picked_qty += flt(so_item.picked_qty)
+			total_qty += flt(so_item.stock_qty)
+		per_picked = total_picked_qty / total_qty * 100
+
+		self.db_set("per_picked", flt(per_picked), update_modified=False)
+
 	def set_indicator(self):
 		"""Set indicator for portal"""
 		if self.per_billed < 100 and self.per_delivered < 100:
@@ -1232,9 +1242,30 @@
 
 @frappe.whitelist()
 def create_pick_list(source_name, target_doc=None):
-	def update_item_quantity(source, target, source_parent):
-		target.qty = flt(source.qty) - flt(source.delivered_qty)
-		target.stock_qty = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.conversion_factor)
+	from erpnext.stock.doctype.packed_item.packed_item import is_product_bundle
+
+	def update_item_quantity(source, target, source_parent) -> None:
+		picked_qty = flt(source.picked_qty) / (flt(source.conversion_factor) or 1)
+		qty_to_be_picked = flt(source.qty) - max(picked_qty, flt(source.delivered_qty))
+
+		target.qty = qty_to_be_picked
+		target.stock_qty = qty_to_be_picked * flt(source.conversion_factor)
+
+	def update_packed_item_qty(source, target, source_parent) -> None:
+		qty = flt(source.qty)
+		for item in source_parent.items:
+			if source.parent_detail_docname == item.name:
+				picked_qty = flt(item.picked_qty) / (flt(item.conversion_factor) or 1)
+				pending_percent = (item.qty - max(picked_qty, item.delivered_qty)) / item.qty
+				target.qty = target.stock_qty = qty * pending_percent
+				return
+
+	def should_pick_order_item(item) -> bool:
+		return (
+			abs(item.delivered_qty) < abs(item.qty)
+			and item.delivered_by_supplier != 1
+			and not is_product_bundle(item.item_code)
+		)
 
 	doc = get_mapped_doc(
 		"Sales Order",
@@ -1245,8 +1276,17 @@
 				"doctype": "Pick List Item",
 				"field_map": {"parent": "sales_order", "name": "sales_order_item"},
 				"postprocess": update_item_quantity,
-				"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
-				and doc.delivered_by_supplier != 1,
+				"condition": should_pick_order_item,
+			},
+			"Packed Item": {
+				"doctype": "Pick List Item",
+				"field_map": {
+					"parent": "sales_order",
+					"name": "sales_order_item",
+					"parent_detail_docname": "product_bundle_item",
+				},
+				"field_no_map": ["picked_qty"],
+				"postprocess": update_packed_item_qty,
 			},
 		},
 		target_doc,
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 195e964..3797856 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -803,13 +803,15 @@
   {
    "fieldname": "picked_qty",
    "fieldtype": "Float",
-   "label": "Picked Qty"
+   "label": "Picked Qty (in Stock UOM)",
+   "no_copy": 1,
+   "read_only": 1
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-03-15 20:17:33.984799",
+ "modified": "2022-04-27 03:15:34.366563",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Sales Order Item",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 7c4a3f6..005e24c 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -27,7 +27,8 @@
   "column_break_5",
   "allow_multiple_items",
   "allow_against_multiple_purchase_orders",
-  "hide_tax_id"
+  "hide_tax_id",
+  "enable_discount_accounting"
  ],
  "fields": [
   {
@@ -164,6 +165,13 @@
    "fieldname": "editable_bundle_item_rates",
    "fieldtype": "Check",
    "label": "Calculate Product Bundle Price based on Child Items' Rates"
+  },
+  {
+   "default": "0",
+   "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
+   "fieldname": "enable_discount_accounting",
+   "fieldtype": "Check",
+   "label": "Enable Discount Accounting for Selling"
   }
  ],
  "icon": "fa fa-cog",
@@ -171,7 +179,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2022-02-04 15:41:59.939261",
+ "modified": "2022-04-14 16:01:29.405642",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Selling Settings",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py
index 29e4712..6c09894 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.py
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.py
@@ -14,6 +14,7 @@
 	def on_update(self):
 		self.toggle_hide_tax_id()
 		self.toggle_editable_rate_for_bundle_items()
+		self.toggle_discount_accounting_fields()
 
 	def validate(self):
 		for key in [
@@ -58,3 +59,60 @@
 			"Check",
 			validate_fields_for_doctype=False,
 		)
+
+	def toggle_discount_accounting_fields(self):
+		enable_discount_accounting = cint(self.enable_discount_accounting)
+
+		make_property_setter(
+			"Sales Invoice Item",
+			"discount_account",
+			"hidden",
+			not (enable_discount_accounting),
+			"Check",
+			validate_fields_for_doctype=False,
+		)
+		if enable_discount_accounting:
+			make_property_setter(
+				"Sales Invoice Item",
+				"discount_account",
+				"mandatory_depends_on",
+				"eval: doc.discount_amount",
+				"Code",
+				validate_fields_for_doctype=False,
+			)
+		else:
+			make_property_setter(
+				"Sales Invoice Item",
+				"discount_account",
+				"mandatory_depends_on",
+				"",
+				"Code",
+				validate_fields_for_doctype=False,
+			)
+
+		make_property_setter(
+			"Sales Invoice",
+			"additional_discount_account",
+			"hidden",
+			not (enable_discount_accounting),
+			"Check",
+			validate_fields_for_doctype=False,
+		)
+		if enable_discount_accounting:
+			make_property_setter(
+				"Sales Invoice",
+				"additional_discount_account",
+				"mandatory_depends_on",
+				"eval: doc.discount_amount",
+				"Code",
+				validate_fields_for_doctype=False,
+			)
+		else:
+			make_property_setter(
+				"Sales Invoice",
+				"additional_discount_account",
+				"mandatory_depends_on",
+				"",
+				"Code",
+				validate_fields_for_doctype=False,
+			)
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 65e0cbb..7a68386 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -343,9 +343,9 @@
 				toggle_other_sections: (show) => {
 					if (show) {
 						this.item_details.$component.is(':visible') ? this.item_details.$component.css('display', 'none') : '';
-						this.item_selector.$component.css('display', 'none');
+						this.item_selector.toggle_component(false);
 					} else {
-						this.item_selector.$component.css('display', 'flex');
+						this.item_selector.toggle_component(true);
 					}
 				},
 
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 4a99f06..eacf480 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -130,10 +130,10 @@
 			},
 			cols: 5,
 			keys: [
-				[ 1, 2, 3, __('Quantity') ],
-				[ 4, 5, 6, __('Discount') ],
-				[ 7, 8, 9, __('Rate') ],
-				[ '.', 0, __('Delete'), __('Remove') ]
+				[ 1, 2, 3, 'Quantity' ],
+				[ 4, 5, 6, 'Discount' ],
+				[ 7, 8, 9, 'Rate' ],
+				[ '.', 0, 'Delete', 'Remove' ]
 			],
 			css_classes: [
 				[ '', '', '', 'col-span-2' ],
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index b62b27b..7a90fb0 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -179,6 +179,25 @@
 		});
 		this.search_field.toggle_label(false);
 		this.item_group_field.toggle_label(false);
+
+		this.attach_clear_btn();
+	}
+
+	attach_clear_btn() {
+		this.search_field.$wrapper.find('.control-input').append(
+			`<span class="link-btn" style="top: 2px;">
+				<a class="btn-open no-decoration" title="${__("Clear")}">
+					${frappe.utils.icon('close', 'sm')}
+				</a>
+			</span>`
+		);
+
+		this.$clear_search_btn = this.search_field.$wrapper.find('.link-btn');
+
+		this.$clear_search_btn.on('click', 'a', () => {
+			this.set_search_value('');
+			this.search_field.set_focus();
+		});
 	}
 
 	set_search_value(value) {
@@ -252,6 +271,16 @@
 				const search_term = e.target.value;
 				this.filter_items({ search_term });
 			}, 300);
+
+			this.$clear_search_btn.toggle(
+				Boolean(this.search_field.$input.val())
+			);
+		});
+
+		this.search_field.$input.on('focus', () => {
+			this.$clear_search_btn.toggle(
+				Boolean(this.search_field.$input.val())
+			);
 		});
 	}
 
@@ -284,7 +313,7 @@
 			if (this.items.length == 1) {
 				this.$items_container.find(".item-wrapper").click();
 				frappe.utils.play_sound("submit");
-				$(this.search_field.$input[0]).val("").trigger("input");
+				this.set_search_value('');
 			} else if (this.items.length == 0 && this.barcode_scanned) {
 				// only show alert of barcode is scanned and enter is pressed
 				frappe.show_alert({
@@ -293,7 +322,7 @@
 				});
 				frappe.utils.play_sound("error");
 				this.barcode_scanned = false;
-				$(this.search_field.$input[0]).val("").trigger("input");
+				this.set_search_value('');
 			}
 		});
 	}
@@ -350,6 +379,7 @@
 	}
 
 	toggle_component(show) {
-		show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none');
+		this.set_search_value('');
+		this.$component.css('display', show ? 'flex': 'none');
 	}
 };
diff --git a/erpnext/selling/page/point_of_sale/pos_number_pad.js b/erpnext/selling/page/point_of_sale/pos_number_pad.js
index 95293d1..f27b0d5 100644
--- a/erpnext/selling/page/point_of_sale/pos_number_pad.js
+++ b/erpnext/selling/page/point_of_sale/pos_number_pad.js
@@ -25,7 +25,7 @@
 					const fieldname = fieldnames && fieldnames[number] ?
 						fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number;
 
-					return a2 + `<div class="numpad-btn ${class_to_append}" data-button-value="${fieldname}">${number}</div>`;
+					return a2 + `<div class="numpad-btn ${class_to_append}" data-button-value="${fieldname}">${__(number)}</div>`;
 				}, '');
 			}, '');
 		}
diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json
index a700ad8..45e160d 100644
--- a/erpnext/selling/workspace/selling/selling.json
+++ b/erpnext/selling/workspace/selling/selling.json
@@ -5,7 +5,7 @@
    "label": "Sales Order Trends"
   }
  ],
- "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Selling\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Quick Access</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order Analysis\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Selling\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Selling\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Sales Order Trends\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Quick Access</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Order Analysis\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Selling\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Items and Pricing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
  "creation": "2020-01-28 11:49:12.092882",
  "docstatus": 0,
  "doctype": "Workspace",
@@ -317,115 +317,8 @@
   {
    "hidden": 0,
    "is_query_report": 0,
-   "label": "Key Reports",
-   "link_count": 0,
-   "onboard": 0,
-   "type": "Card Break"
-  },
-  {
-   "dependencies": "",
-   "hidden": 0,
-   "is_query_report": 1,
-   "label": "Sales Analytics",
-   "link_count": 0,
-   "link_to": "Sales Analytics",
-   "link_type": "Report",
-   "onboard": 1,
-   "type": "Link"
-  },
-  {
-   "dependencies": "Sales Order",
-   "hidden": 0,
-   "is_query_report": 1,
-   "label": "Sales Order Analysis",
-   "link_count": 0,
-   "link_to": "Sales Order Analysis",
-   "link_type": "Report",
-   "onboard": 1,
-   "type": "Link"
-  },
-  {
-   "dependencies": "",
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Sales Funnel",
-   "link_count": 0,
-   "link_to": "sales-funnel",
-   "link_type": "Page",
-   "onboard": 1,
-   "type": "Link"
-  },
-  {
-   "dependencies": "Sales Order",
-   "hidden": 0,
-   "is_query_report": 1,
-   "label": "Sales Order Trends",
-   "link_count": 0,
-   "link_to": "Sales Order Trends",
-   "link_type": "Report",
-   "onboard": 0,
-   "type": "Link"
-  },
-  {
-   "dependencies": "Quotation",
-   "hidden": 0,
-   "is_query_report": 1,
-   "label": "Quotation Trends",
-   "link_count": 0,
-   "link_to": "Quotation Trends",
-   "link_type": "Report",
-   "onboard": 0,
-   "type": "Link"
-  },
-  {
-   "dependencies": "Customer",
-   "hidden": 0,
-   "is_query_report": 1,
-   "label": "Customer Acquisition and Loyalty",
-   "link_count": 0,
-   "link_to": "Customer Acquisition and Loyalty",
-   "link_type": "Report",
-   "onboard": 0,
-   "type": "Link"
-  },
-  {
-   "dependencies": "Sales Order",
-   "hidden": 0,
-   "is_query_report": 1,
-   "label": "Inactive Customers",
-   "link_count": 0,
-   "link_to": "Inactive Customers",
-   "link_type": "Report",
-   "onboard": 0,
-   "type": "Link"
-  },
-  {
-   "dependencies": "Sales Order",
-   "hidden": 0,
-   "is_query_report": 1,
-   "label": "Sales Person-wise Transaction Summary",
-   "link_count": 0,
-   "link_to": "Sales Person-wise Transaction Summary",
-   "link_type": "Report",
-   "onboard": 0,
-   "type": "Link"
-  },
-  {
-   "dependencies": "Item",
-   "hidden": 0,
-   "is_query_report": 1,
-   "label": "Item-wise Sales History",
-   "link_count": 0,
-   "link_to": "Item-wise Sales History",
-   "link_type": "Report",
-   "onboard": 0,
-   "type": "Link"
-  },
-  {
-   "hidden": 0,
-   "is_query_report": 0,
    "label": "Other Reports",
-   "link_count": 0,
+   "link_count": 12,
    "onboard": 0,
    "type": "Card Break"
   },
@@ -560,9 +453,258 @@
    "link_type": "Report",
    "onboard": 0,
    "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Key Reports",
+   "link_count": 22,
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Sales Analytics",
+   "link_count": 0,
+   "link_to": "Sales Analytics",
+   "link_type": "Report",
+   "onboard": 1,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Sales Order",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Sales Order Analysis",
+   "link_count": 0,
+   "link_to": "Sales Order Analysis",
+   "link_type": "Report",
+   "onboard": 1,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Sales Funnel",
+   "link_count": 0,
+   "link_to": "sales-funnel",
+   "link_type": "Page",
+   "onboard": 1,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Sales Order",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Sales Order Trends",
+   "link_count": 0,
+   "link_to": "Sales Order Trends",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Quotation",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Quotation Trends",
+   "link_count": 0,
+   "link_to": "Quotation Trends",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Customer",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Customer Acquisition and Loyalty",
+   "link_count": 0,
+   "link_to": "Customer Acquisition and Loyalty",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Sales Order",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Inactive Customers",
+   "link_count": 0,
+   "link_to": "Inactive Customers",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Sales Order",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Sales Person-wise Transaction Summary",
+   "link_count": 0,
+   "link_to": "Sales Person-wise Transaction Summary",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Item",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Item-wise Sales History",
+   "link_count": 0,
+   "link_to": "Item-wise Sales History",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Lead",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Lead Details",
+   "link_count": 0,
+   "link_to": "Lead Details",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Address",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Customer Addresses And Contacts",
+   "link_count": 0,
+   "link_to": "Address And Contacts",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Item",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Available Stock for Packing Items",
+   "link_count": 0,
+   "link_to": "Available Stock for Packing Items",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Sales Order",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Pending SO Items For Purchase Request",
+   "link_count": 0,
+   "link_to": "Pending SO Items For Purchase Request",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Delivery Note",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Delivery Note Trends",
+   "link_count": 0,
+   "link_to": "Delivery Note Trends",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Sales Invoice",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Sales Invoice Trends",
+   "link_count": 0,
+   "link_to": "Sales Invoice Trends",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Customer",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Customer Credit Balance",
+   "link_count": 0,
+   "link_to": "Customer Credit Balance",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Customer",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Customers Without Any Sales Transactions",
+   "link_count": 0,
+   "link_to": "Customers Without Any Sales Transactions",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Customer",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Sales Partners Commission",
+   "link_count": 0,
+   "link_to": "Sales Partners Commission",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Sales Order",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Territory Target Variance Based On Item Group",
+   "link_count": 0,
+   "link_to": "Territory Target Variance Based On Item Group",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Sales Order",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Sales Person Target Variance Based On Item Group",
+   "link_count": 0,
+   "link_to": "Sales Person Target Variance Based On Item Group",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Sales Order",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Sales Partner Target Variance Based On Item Group",
+   "link_count": 0,
+   "link_to": "Sales Partner Target Variance based on Item Group",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Payment Terms Status for Sales Order",
+   "link_count": 0,
+   "link_to": "Payment Terms Status for Sales Order",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
   }
  ],
- "modified": "2022-01-13 17:43:02.778627",
+ "modified": "2022-04-26 13:29:55.087240",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Selling",
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index b5a4557..f97e7ca 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -78,56 +78,6 @@
 
 		self.assertFalse(get_gl_entries("Delivery Note", dn.name))
 
-	# def test_delivery_note_gl_entry(self):
-	# 	company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
-
-	# 	set_valuation_method("_Test Item", "FIFO")
-
-	# 	make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
-
-	# 	stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory')
-	# 	prev_bal = get_balance_on(stock_in_hand_account)
-
-	# 	dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1")
-
-	# 	gl_entries = get_gl_entries("Delivery Note", dn.name)
-	# 	self.assertTrue(gl_entries)
-
-	# 	stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry",
-	# 		{"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference"))
-
-	# 	expected_values = {
-	# 		stock_in_hand_account: [0.0, stock_value_difference],
-	# 		"Cost of Goods Sold - TCP1": [stock_value_difference, 0.0]
-	# 	}
-	# 	for i, gle in enumerate(gl_entries):
-	# 		self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account))
-
-	# 	# check stock in hand balance
-	# 	bal = get_balance_on(stock_in_hand_account)
-	# 	self.assertEqual(bal, prev_bal - stock_value_difference)
-
-	# 	# back dated incoming entry
-	# 	make_stock_entry(posting_date=add_days(nowdate(), -2), target="Stores - TCP1",
-	# 		qty=5, basic_rate=100)
-
-	# 	gl_entries = get_gl_entries("Delivery Note", dn.name)
-	# 	self.assertTrue(gl_entries)
-
-	# 	stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry",
-	# 		{"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference"))
-
-	# 	expected_values = {
-	# 		stock_in_hand_account: [0.0, stock_value_difference],
-	# 		"Cost of Goods Sold - TCP1": [stock_value_difference, 0.0]
-	# 	}
-	# 	for i, gle in enumerate(gl_entries):
-	# 		self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account))
-
-	# 	dn.cancel()
-	# 	self.assertTrue(get_gl_entries("Delivery Note", dn.name))
-	# 	set_perpetual_inventory(0, company)
-
 	def test_delivery_note_gl_entry_packing_item(self):
 		company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
 
@@ -854,8 +804,6 @@
 			company="_Test Company with perpetual inventory",
 		)
 
-		company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
-
 		set_valuation_method("_Test Item", "FIFO")
 
 		make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
@@ -881,8 +829,6 @@
 	def test_delivery_note_cost_center_with_balance_sheet_account(self):
 		cost_center = "Main - TCP1"
 
-		company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
-
 		set_valuation_method("_Test Item", "FIFO")
 
 		make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py
index 562f7b9..bcd31ad 100644
--- a/erpnext/stock/doctype/item_price/item_price.py
+++ b/erpnext/stock/doctype/item_price/item_price.py
@@ -5,6 +5,8 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
+from frappe.query_builder import Criterion
+from frappe.query_builder.functions import Cast_
 from frappe.utils import getdate
 
 
@@ -48,35 +50,57 @@
 			)
 
 	def check_duplicates(self):
-		conditions = (
-			"""where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s"""
-		)
 
-		for field in [
+		item_price = frappe.qb.DocType("Item Price")
+
+		query = (
+			frappe.qb.from_(item_price)
+			.select(item_price.price_list_rate)
+			.where(
+				(item_price.item_code == self.item_code)
+				& (item_price.price_list == self.price_list)
+				& (item_price.name != self.name)
+			)
+		)
+		data_fields = (
 			"uom",
 			"valid_from",
 			"valid_upto",
-			"packing_unit",
 			"customer",
 			"supplier",
 			"batch_no",
-		]:
-			if self.get(field):
-				conditions += " and {0} = %({0})s ".format(field)
-			else:
-				conditions += "and (isnull({0}) or {0} = '')".format(field)
-
-		price_list_rate = frappe.db.sql(
-			"""
-				select price_list_rate
-				from `tabItem Price`
-				{conditions}
-			""".format(
-				conditions=conditions
-			),
-			self.as_dict(),
 		)
 
+		number_fields = ["packing_unit"]
+
+		for field in data_fields:
+			if self.get(field):
+				query = query.where(item_price[field] == self.get(field))
+			else:
+				query = query.where(
+					Criterion.any(
+						[
+							item_price[field].isnull(),
+							Cast_(item_price[field], "varchar") == "",
+						]
+					)
+				)
+
+		for field in number_fields:
+			if self.get(field):
+				query = query.where(item_price[field] == self.get(field))
+			else:
+				query = query.where(
+					Criterion.any(
+						[
+							item_price[field].isnull(),
+							item_price[field] == 0,
+						]
+					)
+				)
+
+		price_list_rate = query.run(as_dict=True)
+
 		if price_list_rate:
 			frappe.throw(
 				_(
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index e94c34d..cb8eb30 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -29,6 +29,7 @@
   "ordered_qty",
   "column_break_16",
   "incoming_rate",
+  "picked_qty",
   "page_break",
   "prevdoc_doctype",
   "parent_detail_docname"
@@ -234,13 +235,20 @@
    "label": "Ordered Qty",
    "no_copy": 1,
    "read_only": 1
+  },
+  {
+   "fieldname": "picked_qty",
+   "fieldtype": "Float",
+   "label": "Picked Qty",
+   "no_copy": 1,
+   "read_only": 1
   }
  ],
  "idx": 1,
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-03-10 15:42:00.265915",
+ "modified": "2022-04-27 05:23:08.683245",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Packed Item",
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index 026dd4e..4d05d7a 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -32,7 +32,7 @@
 	reset = reset_packing_list(doc)
 
 	for item_row in doc.get("items"):
-		if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}):
+		if is_product_bundle(item_row.item_code):
 			for bundle_item in get_product_bundle_items(item_row.item_code):
 				pi_row = add_packed_item_row(
 					doc=doc,
@@ -54,6 +54,10 @@
 		set_product_bundle_rate_amount(doc, parent_items_price)  # set price in bundle item
 
 
+def is_product_bundle(item_code: str) -> bool:
+	return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code}))
+
+
 def get_indexed_packed_items_table(doc):
 	"""
 	Create dict from stale packed items table like:
diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py
index fe1b0d9..ad7fd9a 100644
--- a/erpnext/stock/doctype/packed_item/test_packed_item.py
+++ b/erpnext/stock/doctype/packed_item/test_packed_item.py
@@ -1,10 +1,12 @@
 # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
+from typing import List, Optional, Tuple
+
+import frappe
 from frappe.tests.utils import FrappeTestCase, change_settings
 from frappe.utils import add_to_date, nowdate
 
-from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
 from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
 from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
 from erpnext.stock.doctype.item.test_item import make_item
@@ -12,6 +14,33 @@
 from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
 
 
+def create_product_bundle(
+	quantities: Optional[List[int]] = None, warehouse: Optional[str] = None
+) -> Tuple[str, List[str]]:
+	"""Get a new product_bundle for use in tests.
+
+	Create 10x required stock if warehouse is specified.
+	"""
+	if not quantities:
+		quantities = [2, 2]
+
+	bundle = make_item(properties={"is_stock_item": 0}).name
+
+	bundle_doc = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": bundle})
+
+	components = []
+	for qty in quantities:
+		compoenent = make_item().name
+		components.append(compoenent)
+		bundle_doc.append("items", {"item_code": compoenent, "qty": qty})
+		if warehouse:
+			make_stock_entry(item=compoenent, to_warehouse=warehouse, qty=10 * qty, rate=100)
+
+	bundle_doc.insert()
+
+	return bundle, components
+
+
 class TestPackedItem(FrappeTestCase):
 	"Test impact on Packed Items table in various scenarios."
 
@@ -19,24 +48,11 @@
 	def setUpClass(cls) -> None:
 		super().setUpClass()
 		cls.warehouse = "_Test Warehouse - _TC"
-		cls.bundle = "_Test Product Bundle X"
-		cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"]
 
-		cls.bundle2 = "_Test Product Bundle Y"
-		cls.bundle2_items = ["_Test Bundle Item 3", "_Test Bundle Item 4"]
+		cls.bundle, cls.bundle_items = create_product_bundle(warehouse=cls.warehouse)
+		cls.bundle2, cls.bundle2_items = create_product_bundle(warehouse=cls.warehouse)
 
-		make_item(cls.bundle, {"is_stock_item": 0})
-		make_item(cls.bundle2, {"is_stock_item": 0})
-		for item in cls.bundle_items + cls.bundle2_items:
-			make_item(item, {"is_stock_item": 1})
-
-		make_item("_Test Normal Stock Item", {"is_stock_item": 1})
-
-		make_product_bundle(cls.bundle, cls.bundle_items, qty=2)
-		make_product_bundle(cls.bundle2, cls.bundle2_items, qty=2)
-
-		for item in cls.bundle_items + cls.bundle2_items:
-			make_stock_entry(item_code=item, to_warehouse=cls.warehouse, qty=100, rate=100)
+		cls.normal_item = make_item().name
 
 	def test_adding_bundle_item(self):
 		"Test impact on packed items if bundle item row is added."
@@ -58,7 +74,7 @@
 		self.assertEqual(so.packed_items[1].qty, 4)
 
 		# change item code to non bundle item
-		so.items[0].item_code = "_Test Normal Stock Item"
+		so.items[0].item_code = self.normal_item
 		so.save()
 
 		self.assertEqual(len(so.packed_items), 0)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
index c604c71..e984c08 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.json
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -114,6 +114,7 @@
    "set_only_once": 1
   },
   {
+   "collapsible": 1,
    "fieldname": "print_settings_section",
    "fieldtype": "Section Break",
    "label": "Print Settings"
@@ -129,7 +130,7 @@
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2021-10-05 15:08:40.369957",
+ "modified": "2022-04-21 07:56:40.646473",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Pick List",
@@ -199,5 +200,6 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 33d7745..70d2f23 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -4,13 +4,14 @@
 import json
 from collections import OrderedDict, defaultdict
 from itertools import groupby
-from operator import itemgetter
+from typing import Dict, List, Set
 
 import frappe
 from frappe import _
 from frappe.model.document import Document
 from frappe.model.mapper import map_child_doc
 from frappe.utils import cint, floor, flt, today
+from frappe.utils.nestedset import get_descendants_of
 
 from erpnext.selling.doctype.sales_order.sales_order import (
 	make_delivery_note as create_delivery_note_from_sales_order,
@@ -38,6 +39,7 @@
 				)
 
 	def before_submit(self):
+		update_sales_orders = set()
 		for item in self.locations:
 			# if the user has not entered any picked qty, set it to stock_qty, before submit
 			if item.picked_qty == 0:
@@ -45,7 +47,8 @@
 
 			if item.sales_order_item:
 				# update the picked_qty in SO Item
-				self.update_so(item.sales_order_item, item.picked_qty, item.item_code)
+				self.update_sales_order_item(item, item.picked_qty, item.item_code)
+				update_sales_orders.add(item.sales_order)
 
 			if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
 				continue
@@ -65,18 +68,29 @@
 				title=_("Quantity Mismatch"),
 			)
 
+		self.update_bundle_picked_qty()
+		self.update_sales_order_picking_status(update_sales_orders)
+
 	def before_cancel(self):
-		# update picked_qty in SO Item on cancel of PL
+		"""Deduct picked qty on cancelling pick list"""
+		updated_sales_orders = set()
+
 		for item in self.get("locations"):
 			if item.sales_order_item:
-				self.update_so(item.sales_order_item, -1 * item.picked_qty, item.item_code)
+				self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code)
+				updated_sales_orders.add(item.sales_order)
 
-	def update_so(self, so_item, picked_qty, item_code):
-		so_doc = frappe.get_doc(
-			"Sales Order", frappe.db.get_value("Sales Order Item", so_item, "parent")
-		)
+		self.update_bundle_picked_qty()
+		self.update_sales_order_picking_status(updated_sales_orders)
+
+	def update_sales_order_item(self, item, picked_qty, item_code):
+		item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item"
+		stock_qty_field = "stock_qty" if not item.product_bundle_item else "qty"
+
 		already_picked, actual_qty = frappe.db.get_value(
-			"Sales Order Item", so_item, ["picked_qty", "qty"]
+			item_table,
+			item.sales_order_item,
+			["picked_qty", stock_qty_field],
 		)
 
 		if self.docstatus == 1:
@@ -86,20 +100,16 @@
 				frappe.throw(
 					_(
 						"You are picking more than required quantity for {}. Check if there is any other pick list created for {}"
-					).format(item_code, so_doc.name)
+					).format(item_code, item.sales_order)
 				)
 
-		frappe.db.set_value("Sales Order Item", so_item, "picked_qty", already_picked + picked_qty)
+		frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty)
 
-		total_picked_qty = 0
-		total_so_qty = 0
-		for item in so_doc.get("items"):
-			total_picked_qty += flt(item.picked_qty)
-			total_so_qty += flt(item.stock_qty)
-		total_picked_qty = total_picked_qty + picked_qty
-		per_picked = total_picked_qty / total_so_qty * 100
-
-		so_doc.db_set("per_picked", flt(per_picked), update_modified=False)
+	@staticmethod
+	def update_sales_order_picking_status(sales_orders: Set[str]) -> None:
+		for sales_order in sales_orders:
+			if sales_order:
+				frappe.get_doc("Sales Order", sales_order).update_picking_status()
 
 	@frappe.whitelist()
 	def set_item_locations(self, save=False):
@@ -109,7 +119,7 @@
 
 		from_warehouses = None
 		if self.parent_warehouse:
-			from_warehouses = frappe.db.get_descendants("Warehouse", self.parent_warehouse)
+			from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse)
 
 		# Create replica before resetting, to handle empty table on update after submit.
 		locations_replica = self.get("locations")
@@ -190,8 +200,7 @@
 			frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
 
 	def before_print(self, settings=None):
-		if self.get("group_same_items"):
-			self.group_similar_items()
+		self.group_similar_items()
 
 	def group_similar_items(self):
 		group_item_qty = defaultdict(float)
@@ -217,6 +226,57 @@
 		for idx, item in enumerate(self.locations, start=1):
 			item.idx = idx
 
+	def update_bundle_picked_qty(self):
+		product_bundles = self._get_product_bundles()
+		product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values())
+
+		for so_row, item_code in product_bundles.items():
+			picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code])
+			item_table = "Sales Order Item"
+			already_picked = frappe.db.get_value(item_table, so_row, "picked_qty")
+			frappe.db.set_value(
+				item_table,
+				so_row,
+				"picked_qty",
+				already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)),
+			)
+
+	def _get_product_bundles(self) -> Dict[str, str]:
+		# Dict[so_item_row: item_code]
+		product_bundles = {}
+		for item in self.locations:
+			if not item.product_bundle_item:
+				continue
+			product_bundles[item.product_bundle_item] = frappe.db.get_value(
+				"Sales Order Item",
+				item.product_bundle_item,
+				"item_code",
+			)
+		return product_bundles
+
+	def _get_product_bundle_qty_map(self, bundles: List[str]) -> Dict[str, Dict[str, float]]:
+		# 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})
+			product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items}
+		return product_bundle_qty_map
+
+	def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int:
+		"""Compute how many full bundles can be created from picked items."""
+		precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction")
+
+		possible_bundles = []
+		for item in self.locations:
+			if item.product_bundle_item != bundle_row:
+				continue
+
+			if qty_in_bundle := bundle_items.get(item.item_code):
+				possible_bundles.append(item.picked_qty / qty_in_bundle)
+			else:
+				possible_bundles.append(0)
+		return int(flt(min(possible_bundles), precision or 6))
+
 
 def validate_item_locations(pick_list):
 	if not pick_list.locations:
@@ -450,22 +510,18 @@
 	for location in pick_list.locations:
 		if location.sales_order:
 			sales_orders.append(
-				[frappe.db.get_value("Sales Order", location.sales_order, "customer"), location.sales_order]
+				frappe.db.get_value(
+					"Sales Order", location.sales_order, ["customer", "name as sales_order"], as_dict=True
+				)
 			)
-	# Group sales orders by customer
-	for key, keydata in groupby(sales_orders, key=itemgetter(0)):
-		sales_dict[key] = set([d[1] for d in keydata])
+
+	for customer, rows in groupby(sales_orders, key=lambda so: so["customer"]):
+		sales_dict[customer] = {row.sales_order for row in rows}
 
 	if sales_dict:
 		delivery_note = create_dn_with_so(sales_dict, pick_list)
 
-	is_item_wo_so = 0
-	for location in pick_list.locations:
-		if not location.sales_order:
-			is_item_wo_so = 1
-			break
-	if is_item_wo_so == 1:
-		# Create a DN for items without sales orders as well
+	if not all(item.sales_order for item in pick_list.locations):
 		delivery_note = create_dn_wo_so(pick_list)
 
 	frappe.msgprint(_("Delivery Note(s) created for the Pick List"))
@@ -492,27 +548,30 @@
 def create_dn_with_so(sales_dict, pick_list):
 	delivery_note = None
 
+	item_table_mapper = {
+		"doctype": "Delivery Note Item",
+		"field_map": {
+			"rate": "rate",
+			"name": "so_detail",
+			"parent": "against_sales_order",
+		},
+		"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
+		and doc.delivered_by_supplier != 1,
+	}
+
 	for customer in sales_dict:
 		for so in sales_dict[customer]:
 			delivery_note = None
 			delivery_note = create_delivery_note_from_sales_order(so, delivery_note, skip_item_mapping=True)
-
-			item_table_mapper = {
-				"doctype": "Delivery Note Item",
-				"field_map": {
-					"rate": "rate",
-					"name": "so_detail",
-					"parent": "against_sales_order",
-				},
-				"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
-				and doc.delivered_by_supplier != 1,
-			}
 			break
 		if delivery_note:
 			# map all items of all sales orders of that customer
 			for so in sales_dict[customer]:
 				map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
-			delivery_note.insert(ignore_mandatory=True)
+			delivery_note.flags.ignore_mandatory = True
+			delivery_note.insert()
+			update_packed_item_details(pick_list, delivery_note)
+			delivery_note.save()
 
 	return delivery_note
 
@@ -520,28 +579,28 @@
 def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
 
 	for location in pick_list.locations:
-		if location.sales_order == sales_order:
-			if location.sales_order_item:
-				sales_order_item = frappe.get_cached_doc(
-					"Sales Order Item", {"name": location.sales_order_item}
-				)
-			else:
-				sales_order_item = None
+		if location.sales_order != sales_order or location.product_bundle_item:
+			continue
 
-			source_doc, table_mapper = (
-				[sales_order_item, item_mapper] if sales_order_item else [location, item_mapper]
-			)
+		if location.sales_order_item:
+			sales_order_item = frappe.get_doc("Sales Order Item", location.sales_order_item)
+		else:
+			sales_order_item = None
 
-			dn_item = map_child_doc(source_doc, delivery_note, table_mapper)
+		source_doc = sales_order_item or location
 
-			if dn_item:
-				dn_item.pick_list_item = location.name
-				dn_item.warehouse = location.warehouse
-				dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
-				dn_item.batch_no = location.batch_no
-				dn_item.serial_no = location.serial_no
+		dn_item = map_child_doc(source_doc, delivery_note, item_mapper)
 
-				update_delivery_note_item(source_doc, dn_item, delivery_note)
+		if dn_item:
+			dn_item.pick_list_item = location.name
+			dn_item.warehouse = location.warehouse
+			dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
+			dn_item.batch_no = location.batch_no
+			dn_item.serial_no = location.serial_no
+
+			update_delivery_note_item(source_doc, dn_item, delivery_note)
+
+	add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper)
 	set_delivery_note_missing_values(delivery_note)
 
 	delivery_note.pick_list = pick_list.name
@@ -549,6 +608,50 @@
 	delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
 
 
+def add_product_bundles_to_delivery_note(
+	pick_list: "PickList", delivery_note, item_mapper
+) -> None:
+	"""Add product bundles found in pick list to delivery note.
+
+	When mapping pick list items, the bundle item itself isn't part of the
+	locations. Dynamically fetch and add parent bundle item into DN."""
+	product_bundles = pick_list._get_product_bundles()
+	product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values())
+
+	for so_row, item_code in product_bundles.items():
+		sales_order_item = frappe.get_doc("Sales Order Item", so_row)
+		dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
+		dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
+			so_row, product_bundle_qty_map[item_code]
+		)
+		update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
+
+
+def update_packed_item_details(pick_list: "PickList", delivery_note) -> None:
+	"""Update stock details on packed items table of delivery note."""
+
+	def _find_so_row(packed_item):
+		for item in delivery_note.items:
+			if packed_item.parent_detail_docname == item.name:
+				return item.so_detail
+
+	def _find_pick_list_location(bundle_row, packed_item):
+		if not bundle_row:
+			return
+		for loc in pick_list.locations:
+			if loc.product_bundle_item == bundle_row and loc.item_code == packed_item.item_code:
+				return loc
+
+	for packed_item in delivery_note.packed_items:
+		so_row = _find_so_row(packed_item)
+		location = _find_pick_list_location(so_row, packed_item)
+		if not location:
+			continue
+		packed_item.warehouse = location.warehouse
+		packed_item.batch_no = location.batch_no
+		packed_item.serial_no = location.serial_no
+
+
 @frappe.whitelist()
 def create_stock_entry(pick_list):
 	pick_list = frappe.get_doc(json.loads(pick_list))
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 27b06d2..f552299 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -3,18 +3,21 @@
 
 import frappe
 from frappe import _dict
-
-test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"]
-
 from frappe.tests.utils import FrappeTestCase
 
+from erpnext.selling.doctype.sales_order.sales_order import create_pick_list
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
 from erpnext.stock.doctype.item.test_item import create_item, make_item
+from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle
 from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
 from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
 from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
 	EmptyStockReconciliationItemsError,
 )
 
+test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"]
+
 
 class TestPickList(FrappeTestCase):
 	def test_pick_list_picks_warehouse_for_each_item(self):
@@ -579,14 +582,79 @@
 				if dn_item.item_code == "_Test Item 2":
 					self.assertEqual(dn_item.qty, 2)
 
-	# def test_pick_list_skips_items_in_expired_batch(self):
-	# 	pass
+	def test_picklist_with_multi_uom(self):
+		warehouse = "_Test Warehouse - _TC"
+		item = make_item(properties={"uoms": [dict(uom="Box", conversion_factor=24)]}).name
+		make_stock_entry(item=item, to_warehouse=warehouse, qty=1000)
 
-	# def test_pick_list_from_sales_order(self):
-	# 	pass
+		so = make_sales_order(item_code=item, qty=10, rate=42, uom="Box")
+		pl = create_pick_list(so.name)
+		# pick half the qty
+		for loc in pl.locations:
+			loc.picked_qty = loc.stock_qty / 2
+		pl.save()
+		pl.submit()
 
-	# def test_pick_list_from_work_order(self):
-	# 	pass
+		so.reload()
+		self.assertEqual(so.per_picked, 50)
 
-	# def test_pick_list_from_material_request(self):
-	# 	pass
+	def test_picklist_with_bundles(self):
+		warehouse = "_Test Warehouse - _TC"
+
+		quantities = [5, 2]
+		bundle, components = create_product_bundle(quantities, warehouse=warehouse)
+		bundle_items = dict(zip(components, quantities))
+
+		so = make_sales_order(item_code=bundle, qty=3, rate=42)
+
+		pl = create_pick_list(so.name)
+		pl.save()
+		self.assertEqual(len(pl.locations), 2)
+		for item in pl.locations:
+			self.assertEqual(item.stock_qty, bundle_items[item.item_code] * 3)
+
+		# check picking status on sales order
+		pl.submit()
+		so.reload()
+		self.assertEqual(so.per_picked, 100)
+
+		# deliver
+		dn = create_delivery_note(pl.name).submit()
+		self.assertEqual(dn.items[0].rate, 42)
+		self.assertEqual(dn.packed_items[0].warehouse, warehouse)
+		so.reload()
+		self.assertEqual(so.per_delivered, 100)
+
+	def test_picklist_with_partial_bundles(self):
+		# from test_records.json
+		warehouse = "_Test Warehouse - _TC"
+
+		quantities = [5, 2]
+		bundle, components = create_product_bundle(quantities, warehouse=warehouse)
+
+		so = make_sales_order(item_code=bundle, qty=4, rate=42)
+
+		pl = create_pick_list(so.name)
+		for loc in pl.locations:
+			loc.picked_qty = loc.qty / 2
+
+		pl.save().submit()
+		so.reload()
+		self.assertEqual(so.per_picked, 50)
+
+		# deliver half qty
+		dn = create_delivery_note(pl.name).submit()
+		self.assertEqual(dn.items[0].rate, 42)
+		so.reload()
+		self.assertEqual(so.per_delivered, 50)
+
+		pl = create_pick_list(so.name)
+		pl.save().submit()
+		so.reload()
+		self.assertEqual(so.per_picked, 100)
+
+		# deliver remaining
+		dn = create_delivery_note(pl.name).submit()
+		self.assertEqual(dn.items[0].rate, 42)
+		so.reload()
+		self.assertEqual(so.per_delivered, 100)
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
index 805286d..a96ebfc 100644
--- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
@@ -27,6 +27,7 @@
   "column_break_15",
   "sales_order",
   "sales_order_item",
+  "product_bundle_item",
   "material_request",
   "material_request_item"
  ],
@@ -146,6 +147,7 @@
   {
    "fieldname": "sales_order_item",
    "fieldtype": "Data",
+   "hidden": 1,
    "label": "Sales Order Item",
    "read_only": 1
   },
@@ -177,11 +179,19 @@
    "fieldtype": "Data",
    "label": "Item Group",
    "read_only": 1
+  },
+  {
+   "description": "product bundle item row's name in sales order. Also indicates that picked item is to be used for a product bundle",
+   "fieldname": "product_bundle_item",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Product Bundle Item",
+   "read_only": 1
   }
  ],
  "istable": 1,
  "links": [],
- "modified": "2021-09-28 12:02:16.923056",
+ "modified": "2022-04-22 05:27:38.497997",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Pick List Item",
@@ -190,5 +200,6 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index c4aa8a4..27a6eaf 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -1167,7 +1167,7 @@
 			from `tabItem` i LEFT JOIN `tabItem Default` id ON i.name=id.parent and id.company=%s
 			where i.name=%s
 				and i.disabled=0
-				and (i.end_of_life is null or i.end_of_life='0000-00-00' or i.end_of_life > %s)""",
+				and (i.end_of_life is null or i.end_of_life<'1900-01-01' or i.end_of_life > %s)""",
 			(self.company, args.get("item_code"), nowdate()),
 			as_dict=1,
 		)
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index b4fac82..5850ec7 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -2,12 +2,12 @@
 # See license.txt
 
 import json
-from datetime import timedelta
 from uuid import uuid4
 
 import frappe
 from frappe.core.page.permission_manager.permission_manager import reset
 from frappe.custom.doctype.property_setter.property_setter import make_property_setter
+from frappe.query_builder.functions import CombineDatetime
 from frappe.tests.utils import FrappeTestCase
 from frappe.utils import add_days, today
 from frappe.utils.data import add_to_date
@@ -1126,6 +1126,63 @@
 		# original amount
 		self.assertEqual(50, _get_stock_credit(final_consumption))
 
+	def test_tie_breaking(self):
+		frappe.flags.dont_execute_stock_reposts = True
+		self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
+
+		item = make_item().name
+		warehouse = "_Test Warehouse - _TC"
+
+		posting_date = "2022-01-01"
+		posting_time = "00:00:01"
+		sle = frappe.qb.DocType("Stock Ledger Entry")
+
+		def ordered_qty_after_transaction():
+			return (
+				frappe.qb.from_(sle)
+				.select("qty_after_transaction")
+				.where((sle.item_code == item) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0))
+				.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
+				.orderby(sle.creation)
+			).run(pluck=True)
+
+		first = make_stock_entry(
+			item_code=item,
+			to_warehouse=warehouse,
+			qty=10,
+			posting_time=posting_time,
+			posting_date=posting_date,
+			do_not_submit=True,
+		)
+		second = make_stock_entry(
+			item_code=item,
+			to_warehouse=warehouse,
+			qty=1,
+			posting_date=posting_date,
+			posting_time=posting_time,
+			do_not_submit=True,
+		)
+
+		first.submit()
+		second.submit()
+
+		self.assertEqual([10, 11], ordered_qty_after_transaction())
+
+		first.cancel()
+		self.assertEqual([1], ordered_qty_after_transaction())
+
+		backdated = make_stock_entry(
+			item_code=item,
+			to_warehouse=warehouse,
+			qty=1,
+			posting_date="2021-01-01",
+			posting_time=posting_time,
+		)
+		self.assertEqual([1, 2], ordered_qty_after_transaction())
+
+		backdated.cancel()
+		self.assertEqual([1], ordered_qty_after_transaction())
+
 
 def create_repack_entry(**args):
 	args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index e826e00..1e59aae 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -30,7 +30,6 @@
 		frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
 
 	def tearDown(self):
-		frappe.flags.dont_execute_stock_reposts = None
 		frappe.local.future_sle = {}
 
 	def test_reco_for_fifo(self):
@@ -40,7 +39,9 @@
 		self._test_reco_sle_gle("Moving Average")
 
 	def _test_reco_sle_gle(self, valuation_method):
-		se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1")
+		item_code = make_item(properties={"valuation_method": valuation_method}).name
+
+		se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1", item_code=item_code)
 		company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
 		# [[qty, valuation_rate, posting_date,
 		# 		posting_time, expected_stock_value, bin_qty, bin_valuation]]
@@ -54,11 +55,9 @@
 		]
 
 		for d in input_data:
-			set_valuation_method("_Test Item", valuation_method)
-
 			last_sle = get_previous_sle(
 				{
-					"item_code": "_Test Item",
+					"item_code": item_code,
 					"warehouse": "Stores - TCP1",
 					"posting_date": d[2],
 					"posting_time": d[3],
@@ -67,6 +66,7 @@
 
 			# submit stock reconciliation
 			stock_reco = create_stock_reconciliation(
+				item_code=item_code,
 				qty=d[0],
 				rate=d[1],
 				posting_date=d[2],
@@ -481,9 +481,11 @@
 		"""
 		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
 
+		frappe.db.rollback()
+
 		# repost will make this test useless, qty should update in realtime without reposts
 		frappe.flags.dont_execute_stock_reposts = True
-		frappe.db.rollback()
+		self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
 
 		item_code = make_item().name
 		warehouse = "_Test Warehouse - _TC"
@@ -594,26 +596,26 @@
 		b.save()
 
 
-def insert_existing_sle(warehouse):
+def insert_existing_sle(warehouse, item_code="_Test Item"):
 	from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
 
 	se1 = make_stock_entry(
 		posting_date="2012-12-15",
 		posting_time="02:00",
-		item_code="_Test Item",
+		item_code=item_code,
 		target=warehouse,
 		qty=10,
 		basic_rate=700,
 	)
 
 	se2 = make_stock_entry(
-		posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item", source=warehouse, qty=15
+		posting_date="2012-12-25", posting_time="03:00", item_code=item_code, source=warehouse, qty=15
 	)
 
 	se3 = make_stock_entry(
 		posting_date="2013-01-05",
 		posting_time="07:00",
-		item_code="_Test Item",
+		item_code=item_code,
 		target=warehouse,
 		qty=15,
 		basic_rate=1200,
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index 3b18a9a..df16643 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -36,6 +36,9 @@
 				self.set_onload("account", account)
 		load_address_and_contact(self)
 
+	def validate(self):
+		self.warn_about_multiple_warehouse_account()
+
 	def on_update(self):
 		self.update_nsm_model()
 
@@ -70,6 +73,53 @@
 		self.update_nsm_model()
 		self.unlink_from_items()
 
+	def warn_about_multiple_warehouse_account(self):
+		"If Warehouse value is split across multiple accounts, warn."
+
+		def get_accounts_where_value_is_booked(name):
+			sle = frappe.qb.DocType("Stock Ledger Entry")
+			gle = frappe.qb.DocType("GL Entry")
+			ac = frappe.qb.DocType("Account")
+
+			return (
+				frappe.qb.from_(sle)
+				.join(gle)
+				.on(sle.voucher_no == gle.voucher_no)
+				.join(ac)
+				.on(ac.name == gle.account)
+				.select(gle.account)
+				.distinct()
+				.where((sle.warehouse == name) & (ac.account_type == "Stock"))
+				.orderby(sle.creation)
+				.run(as_dict=True)
+			)
+
+		if self.is_new():
+			return
+
+		old_wh_account = frappe.db.get_value("Warehouse", self.name, "account")
+
+		# WH account is being changed or set get all accounts against which wh value is booked
+		if self.account != old_wh_account:
+			accounts = get_accounts_where_value_is_booked(self.name)
+			accounts = [d.account for d in accounts]
+
+			if not accounts or (len(accounts) == 1 and self.account in accounts):
+				# if same singular account has stock value booked ignore
+				return
+
+			warning = _("Warehouse's Stock Value has already been booked in the following accounts:")
+			account_str = "<br>" + ", ".join(frappe.bold(ac) for ac in accounts)
+			reason = "<br><br>" + _(
+				"Booking stock value across multiple accounts will make it harder to track stock and account value."
+			)
+
+			frappe.msgprint(
+				warning + account_str + reason,
+				title=_("Multiple Warehouse Accounts"),
+				indicator="orange",
+			)
+
 	def check_if_sle_exists(self):
 		return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name})
 
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index a781479..7e5c231 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -8,9 +8,8 @@
 import frappe
 from frappe import _
 from frappe.model.meta import get_field_precision
-from frappe.query_builder.functions import Sum
+from frappe.query_builder.functions import CombineDatetime, Sum
 from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
-from pypika import CustomFunction
 
 import erpnext
 from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
@@ -1158,16 +1157,15 @@
 	item_code, warehouse, batch_no, posting_date, posting_time, creation=None
 ):
 
-	Timestamp = CustomFunction("timestamp", ["date", "time"])
-
 	sle = frappe.qb.DocType("Stock Ledger Entry")
 
-	timestamp_condition = Timestamp(sle.posting_date, sle.posting_time) < Timestamp(
+	timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
 		posting_date, posting_time
 	)
 	if creation:
 		timestamp_condition |= (
-			Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time)
+			CombineDatetime(sle.posting_date, sle.posting_time)
+			== CombineDatetime(posting_date, posting_time)
 		) & (sle.creation < creation)
 
 	batch_details = (
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index d40218e..2120304 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -7,6 +7,7 @@
 
 import frappe
 from frappe import _
+from frappe.query_builder.functions import CombineDatetime
 from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
 
 import erpnext
@@ -143,12 +144,10 @@
 
 
 def get_serial_nos_data_after_transactions(args):
-	from pypika import CustomFunction
 
 	serial_nos = set()
 	args = frappe._dict(args)
 	sle = frappe.qb.DocType("Stock Ledger Entry")
-	Timestamp = CustomFunction("timestamp", ["date", "time"])
 
 	stock_ledger_entries = (
 		frappe.qb.from_(sle)
@@ -157,7 +156,8 @@
 			(sle.item_code == args.item_code)
 			& (sle.warehouse == args.warehouse)
 			& (
-				Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time)
+				CombineDatetime(sle.posting_date, sle.posting_time)
+				< CombineDatetime(args.posting_date, args.posting_time)
 			)
 			& (sle.is_cancelled == 0)
 		)
diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv
index 6703da6..073820e 100644
--- a/erpnext/translations/ru.csv
+++ b/erpnext/translations/ru.csv
@@ -3034,7 +3034,7 @@
 To Date cannot be before From Date,На сегодняшний день не может быть раньше от даты,
 To Date cannot be less than From Date,"Дата не может быть меньше, чем с даты",
 To Date must be greater than From Date,"До даты должно быть больше, чем с даты",
-To Date should be within the Fiscal Year. Assuming To Date = {0},Дата должна быть в пределах финансового года. Предположим, до даты = {0},
+"To Date should be within the Fiscal Year. Assuming To Date = {0}","Дата должна быть в пределах финансового года. Предположим, до даты = {0}",
 To Datetime,Для DateTime,
 To Deliver,Для доставки,
 To Deliver and Bill,Для доставки и оплаты,
@@ -6917,7 +6917,7 @@
 Working Hours Threshold for Half Day,Порог рабочего времени на полдня,
 Working hours below which Half Day is marked. (Zero to disable),"Рабочее время, ниже которого отмечается полдня. (Ноль отключить)",
 Working Hours Threshold for Absent,Порог рабочего времени для отсутствующих,
-Working hours below which Absent is marked. (Zero to disable),Порог рабочего времени, ниже которого устанавливается отметка «Отсутствует». (Ноль для отключения),",
+"Working hours below which Absent is marked. (Zero to disable)","Порог рабочего времени, ниже которого устанавливается отметка «Отсутствует». (Ноль для отключения)",
 Process Attendance After,Посещаемость процесса после,
 Attendance will be marked automatically only after this date.,Посещаемость будет отмечена автоматически только после этой даты.,
 Last Sync of Checkin,Последняя синхронизация регистрации,