Merge pull request #30542 from deepeshgarg007/disabled_tax_category

fix: Ignore disabled tax categories
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index e9cb6cf..3bc22af 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -26,3 +26,6 @@
 
 # bulk format python code with black
 494bd9ef78313436f0424b918f200dab8fc7c20b
+
+# bulk format python code with black
+baec607ff5905b1c67531096a9cf50ec7ff00a5d
\ No newline at end of file
diff --git a/.mergify.yml b/.mergify.yml
index 315d90f..cc8c080 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -88,3 +88,37 @@
           - version-12-pre-release
         assignees:
           - "{{ author }}"
+
+  - name: Automatic merge on CI success and review
+    conditions:
+      - status-success=linters
+      - status-success=Sider
+      - status-success=Semantic Pull Request
+      - status-success=Patch Test
+      - status-success=Python Unit Tests (1)
+      - status-success=Python Unit Tests (2)
+      - status-success=Python Unit Tests (3)
+      - label!=dont-merge
+      - label!=squash
+      - "#approved-reviews-by>=1"
+    actions:
+      merge:
+        method: merge
+  - name: Automatic squash on CI success and review
+    conditions:
+      - status-success=linters
+      - status-success=Sider
+      - status-success=Patch Test
+      - status-success=Python Unit Tests (1)
+      - status-success=Python Unit Tests (2)
+      - status-success=Python Unit Tests (3)
+      - label!=dont-merge
+      - label=squash
+      - "#approved-reviews-by>=1"
+    actions:
+      merge:
+        method: squash
+        commit_message_template: |
+            {{ title }} (#{{ number }})
+
+            {{ body }}
diff --git a/README.md b/README.md
index 9609353..c26660c 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,7 @@
     </p>
 
 [![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
+[![UI](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml/badge.svg?branch=develop&event=schedule)](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml)
 [![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext)
 [![codecov](https://codecov.io/gh/frappe/erpnext/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](https://codecov.io/gh/frappe/erpnext)
 [![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext-worker.svg)](https://hub.docker.com/r/frappe/erpnext-worker)
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index 150f68b..c71ea36 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -204,7 +204,9 @@
 		if not self.account_currency:
 			self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
 
-		elif self.account_currency != frappe.db.get_value("Account", self.name, "account_currency"):
+		gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
+
+		if gl_currency and self.account_currency != gl_currency:
 			if frappe.db.get_value("GL Entry", {"account": self.name}):
 				frappe.throw(_("Currency can not be changed after making entries using some other currency"))
 
diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js
index a3ef384..8ae90ce 100644
--- a/erpnext/accounts/doctype/account/account_tree.js
+++ b/erpnext/accounts/doctype/account/account_tree.js
@@ -160,7 +160,7 @@
 			let root_company = treeview.page.fields_dict.root_company.get_value();
 
 			if(root_company) {
-				frappe.throw(__("Please add the account to root level Company - ") + root_company);
+				frappe.throw(__("Please add the account to root level Company - {0}"), [root_company]);
 			} else {
 				treeview.new_node();
 			}
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index efc063d..f9c9173 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -241,6 +241,28 @@
 		for doc in to_delete:
 			frappe.delete_doc("Account", doc)
 
+	def test_validate_account_currency(self):
+		from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
+
+		if not frappe.db.get_value("Account", "Test Currency Account - _TC"):
+			acc = frappe.new_doc("Account")
+			acc.account_name = "Test Currency Account"
+			acc.parent_account = "Tax Assets - _TC"
+			acc.company = "_Test Company"
+			acc.insert()
+		else:
+			acc = frappe.get_doc("Account", "Test Currency Account - _TC")
+
+		self.assertEqual(acc.account_currency, "INR")
+
+		# Make a JV against this account
+		make_journal_entry(
+			"Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True
+		)
+
+		acc.account_currency = "USD"
+		self.assertRaises(frappe.ValidationError, acc.save)
+
 
 def _make_test_records(verbose=None):
 	from frappe.test_runner import make_test_objects
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
index 990d6d9..a964965 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
@@ -200,7 +200,7 @@
 				})
 				.then((result) => {
 					if (result.length > 0) {
-						frm.add_custom_button("Report Error", () => {
+						frm.add_custom_button(__("Report Error"), () => {
 							let fake_xhr = {
 								responseText: JSON.stringify({
 									exc: result[0].error,
diff --git a/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py b/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py
index 3bce4d5..402469f 100644
--- a/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py
+++ b/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py
@@ -3,6 +3,7 @@
 
 
 import frappe
+from frappe import _
 from frappe.model.document import Document
 
 
@@ -16,6 +17,6 @@
 		]
 		if len(checked_fields) > 1:
 			frappe.throw(
-				frappe._("You can only select a maximum of one option from the list of check boxes."),
-				title="Error",
+				_("You can only select a maximum of one option from the list of check boxes."),
+				title=_("Error"),
 			)
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
index 04a8e8e..edea37d 100644
--- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
@@ -68,9 +68,8 @@
 					str(key.key).format(transaction_date=nowdate(), to_currency="INR", from_currency="USD")
 				]
 		except Exception:
-			frappe.throw("Invalid result key. Response: " + response.text)
+			frappe.throw(_("Invalid result key. Response:") + " " + response.text)
 		if not isinstance(value, (int, float)):
 			frappe.throw(_("Returned exchange rate is neither integer not float."))
 
 		self.url = response.url
-		frappe.msgprint("Exchange rate of USD to INR is " + str(value))
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.js b/erpnext/accounts/doctype/gl_entry/gl_entry.js
index 491cf4d..4d2a513 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.js
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.js
@@ -3,6 +3,6 @@
 
 frappe.ui.form.on('GL Entry', {
 	refresh: function(frm) {
-
+		frm.page.btn_secondary.hide()
 	}
 });
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index aee7f0e..e5fa57d 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -269,6 +269,11 @@
 		if not self.fiscal_year:
 			self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
 
+	def on_cancel(self):
+		msg = _("Individual GL Entry cannot be cancelled.")
+		msg += "<br>" + _("Please cancel related transaction.")
+		frappe.throw(msg)
+
 
 def validate_balance_type(account, adv_adj=False):
 	if not adv_adj and account:
diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
index d037302..ed35d1e 100644
--- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
+++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
@@ -42,12 +42,7 @@
 			pos_profiles = list(map(lambda x: x[0], pos_profiles))
 
 			if pos_profiles:
-				message = (
-					"POS Profile "
-					+ frappe.bold(", ".join(pos_profiles))
-					+ " contains \
-					Mode of Payment "
-					+ frappe.bold(str(self.name))
-					+ ". Please remove them to disable this mode."
-				)
-				frappe.throw(_(message), title="Not Allowed")
+				message = _(
+					"POS Profile {} contains Mode of Payment {}. Please remove them to disable this mode."
+				).format(frappe.bold(", ".join(pos_profiles)), frappe.bold(str(self.name)))
+				frappe.throw(message, title=_("Not Allowed"))
diff --git a/erpnext/accounts/doctype/party_account/party_account.json b/erpnext/accounts/doctype/party_account/party_account.json
index c9f15a6..6933057 100644
--- a/erpnext/accounts/doctype/party_account/party_account.json
+++ b/erpnext/accounts/doctype/party_account/party_account.json
@@ -3,6 +3,7 @@
  "creation": "2014-08-29 16:02:39.740505",
  "doctype": "DocType",
  "editable_grid": 1,
+ "engine": "InnoDB",
  "field_order": [
   "company",
   "account"
@@ -11,6 +12,7 @@
   {
    "fieldname": "company",
    "fieldtype": "Link",
+   "ignore_user_permissions": 1,
    "in_list_view": 1,
    "label": "Company",
    "options": "Company",
@@ -27,7 +29,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-04-07 18:13:08.833822",
+ "modified": "2022-04-04 12:31:02.994197",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Party Account",
@@ -35,5 +37,6 @@
  "permissions": [],
  "quick_entry": 1,
  "sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index b2b818a..403e2bd 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -224,10 +224,7 @@
 			(frm.doc.total_allocated_amount > party_amount)));
 
 		frm.toggle_display("set_exchange_gain_loss",
-			(frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount &&
-				((frm.doc.paid_from_account_currency != company_currency ||
-					frm.doc.paid_to_account_currency != company_currency) &&
-					frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)));
+			frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
 
 		frm.refresh_fields();
 	},
@@ -532,7 +529,8 @@
 				to_currency: to_currency
 			},
 			callback: function(r, rt) {
-				frm.set_value(exchange_rate_field, r.message);
+				const ex_rate = flt(r.message, frm.get_field(exchange_rate_field).get_precision());
+				frm.set_value(exchange_rate_field, ex_rate);
 			}
 		})
 	},
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py
index 65fd4af..e83dc0f 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py
@@ -61,13 +61,13 @@
 
 		if len(item_groups) != len(set(item_groups)):
 			frappe.throw(
-				_("Duplicate item group found in the item group table"), title="Duplicate Item Group"
+				_("Duplicate item group found in the item group table"), title=_("Duplicate Item Group")
 			)
 
 		if len(customer_groups) != len(set(customer_groups)):
 			frappe.throw(
 				_("Duplicate customer group found in the cutomer group table"),
-				title="Duplicate Customer Group",
+				title=_("Duplicate Customer Group"),
 			)
 
 	def validate_payment_methods(self):
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index c45b069..2438f4b 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -35,10 +35,11 @@
 			self.margin_rate_or_amount = 0.0
 
 	def validate_duplicate_apply_on(self):
-		field = apply_on_dict.get(self.apply_on)
-		values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
-		if len(values) != len(set(values)):
-			frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
+		if self.apply_on != "Transaction":
+			field = apply_on_dict.get(self.apply_on)
+			values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
+			if len(values) != len(set(values)):
+				frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
 
 	def validate_mandatory(self):
 		for apply_on, field in apply_on_dict.items():
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
index 29f2e98..7dd77fb 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
@@ -8,7 +8,7 @@
 	},
 	refresh: function(frm){
 		if(!frm.doc.__islocal) {
-			frm.add_custom_button('Send Emails',function(){
+			frm.add_custom_button(__('Send Emails'), function(){
 				frappe.call({
 					method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_emails",
 					args: {
@@ -24,7 +24,7 @@
 					}
 				});
 			});
-			frm.add_custom_button('Download',function(){
+			frm.add_custom_button(__('Download'), function(){
 				var url = frappe.urllib.get_full_url(
 					'/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?'
 					+ 'document_name='+encodeURIComponent(frm.doc.name))
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 1a398ab..ee29d2a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -141,7 +141,7 @@
 				})
 			}, __("Get Items From"));
 		}
-		this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes");
+		this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted);
 
 		if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
 			frappe.model.with_doc("Supplier", me.frm.doc.supplier, function() {
@@ -276,6 +276,8 @@
 		if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference)
 			return;
 
+		if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return;
+
 		erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details",
 			{
 				posting_date: this.frm.doc.posting_date,
@@ -569,10 +571,10 @@
 	},
 
 	is_subcontracted: function(frm) {
-		if (frm.doc.is_subcontracted === "Yes") {
+		if (frm.doc.is_subcontracted) {
 			erpnext.buying.get_default_bom(frm);
 		}
-		frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted==="Yes");
+		frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted);
 	},
 
 	update_stock: function(frm) {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index bd01164..9f87c5a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -543,11 +543,10 @@
    "fieldtype": "Column Break"
   },
   {
-   "default": "No",
+   "default": "0",
    "fieldname": "is_subcontracted",
-   "fieldtype": "Select",
-   "label": "Raw Materials Supplied",
-   "options": "No\nYes",
+   "fieldtype": "Check",
+   "label": "Is Subcontracted",
    "print_hide": 1
   },
   {
@@ -1366,7 +1365,7 @@
    "width": "50px"
   },
   {
-   "depends_on": "eval:doc.update_stock && doc.is_subcontracted==\"Yes\"",
+   "depends_on": "eval:doc.update_stock && doc.is_subcontracted",
    "fieldname": "supplier_warehouse",
    "fieldtype": "Link",
    "label": "Supplier Warehouse",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 57bc0a7..e6a46d0 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -249,8 +249,9 @@
 
 	def validate_warehouse(self, for_validate=True):
 		if self.update_stock and for_validate:
+			stock_items = self.get_stock_items()
 			for d in self.get("items"):
-				if not d.warehouse:
+				if not d.warehouse and d.item_code in stock_items:
 					frappe.throw(
 						_(
 							"Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}"
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 843f66d..73390dd 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -901,7 +901,7 @@
 		)
 
 		pi = make_purchase_invoice(
-			item_code="_Test FG Item", qty=10, rate=500, update_stock=1, is_subcontracted="Yes"
+			item_code="_Test FG Item", qty=10, rate=500, update_stock=1, is_subcontracted=1
 		)
 
 		self.assertEqual(len(pi.get("supplied_items")), 2)
@@ -1611,7 +1611,7 @@
 	pi.conversion_rate = args.conversion_rate or 1
 	pi.is_return = args.is_return
 	pi.return_against = args.return_against
-	pi.is_subcontracted = args.is_subcontracted or "No"
+	pi.is_subcontracted = args.is_subcontracted or 0
 	pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC"
 	pi.cost_center = args.parent_cost_center
 
@@ -1674,7 +1674,7 @@
 	pi.is_return = args.is_return
 	pi.is_return = args.is_return
 	pi.credit_to = args.return_against or "Creditors - _TC"
-	pi.is_subcontracted = args.is_subcontracted or "No"
+	pi.is_subcontracted = args.is_subcontracted or 0
 	if args.supplier_warehouse:
 		pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
 
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index f9b2efd..6651195 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -623,7 +623,7 @@
   },
   {
    "default": "0",
-   "depends_on": "eval:parent.is_subcontracted == 'Yes'",
+   "depends_on": "eval:parent.is_subcontracted",
    "fieldname": "include_exploded_items",
    "fieldtype": "Check",
    "label": "Include Exploded Items",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index af6a52a..6818955 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -280,6 +280,9 @@
 		}
 		var me = this;
 		if(this.frm.updating_party_details) return;
+
+		if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return;
+
 		erpnext.utils.get_party_details(this.frm,
 			"erpnext.accounts.party.get_party_details", {
 				posting_date: this.frm.doc.posting_date,
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 7d98c22..1efd3dc 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1412,7 +1412,7 @@
 					)
 				)
 			else:
-				frappe.throw(_("Select change amount account"), title="Mandatory Field")
+				frappe.throw(_("Select change amount account"), title=_("Mandatory Field"))
 
 	def make_write_off_gl_entry(self, gl_entries):
 		# write off entries, applicable if only pos
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 35c2f84..a519d8b 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -34,7 +34,9 @@
 
 	def validate_thresholds(self):
 		for d in self.get("rates"):
-			if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold:
+			if (
+				d.cumulative_threshold and d.single_threshold and d.cumulative_threshold < d.single_threshold
+			):
 				frappe.throw(
 					_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(
 						d.idx
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 50f37be..f52e517 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -253,7 +253,7 @@
 	if not from_repost:
 		validate_cwip_accounts(gl_map)
 
-	round_off_debit_credit(gl_map)
+	process_debit_credit_difference(gl_map)
 
 	if gl_map:
 		check_freezing_date(gl_map[0]["posting_date"], adv_adj)
@@ -302,12 +302,29 @@
 				)
 
 
-def round_off_debit_credit(gl_map):
+def process_debit_credit_difference(gl_map):
 	precision = get_field_precision(
 		frappe.get_meta("GL Entry").get_field("debit"),
 		currency=frappe.get_cached_value("Company", gl_map[0].company, "default_currency"),
 	)
 
+	voucher_type = gl_map[0].voucher_type
+	voucher_no = gl_map[0].voucher_no
+	allowance = get_debit_credit_allowance(voucher_type, precision)
+
+	debit_credit_diff = get_debit_credit_difference(gl_map, precision)
+	if abs(debit_credit_diff) > allowance:
+		raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
+
+	elif abs(debit_credit_diff) >= (1.0 / (10**precision)):
+		make_round_off_gle(gl_map, debit_credit_diff, precision)
+
+	debit_credit_diff = get_debit_credit_difference(gl_map, precision)
+	if abs(debit_credit_diff) > allowance:
+		raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
+
+
+def get_debit_credit_difference(gl_map, precision):
 	debit_credit_diff = 0.0
 	for entry in gl_map:
 		entry.debit = flt(entry.debit, precision)
@@ -316,20 +333,24 @@
 
 	debit_credit_diff = flt(debit_credit_diff, precision)
 
-	if gl_map[0]["voucher_type"] in ("Journal Entry", "Payment Entry"):
+	return debit_credit_diff
+
+
+def get_debit_credit_allowance(voucher_type, precision):
+	if voucher_type in ("Journal Entry", "Payment Entry"):
 		allowance = 5.0 / (10**precision)
 	else:
 		allowance = 0.5
 
-	if abs(debit_credit_diff) > allowance:
-		frappe.throw(
-			_("Debit and Credit not equal for {0} #{1}. Difference is {2}.").format(
-				gl_map[0].voucher_type, gl_map[0].voucher_no, debit_credit_diff
-			)
-		)
+	return allowance
 
-	elif abs(debit_credit_diff) >= (1.0 / (10**precision)):
-		make_round_off_gle(gl_map, debit_credit_diff, precision)
+
+def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no):
+	frappe.throw(
+		_("Debit and Credit not equal for {0} #{1}. Difference is {2}.").format(
+			voucher_type, voucher_no, debit_credit_diff
+		)
+	)
 
 
 def make_round_off_gle(gl_map, debit_credit_diff, precision):
diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
index e658049..605ce83 100644
--- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
+++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
@@ -1,7 +1,8 @@
 {%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
-{%- set einvoice = json.loads(doc.signed_einvoice) -%}
 
 <div class="page-break">
+	{% if doc.signed_einvoice %}
+	{%- set einvoice = json.loads(doc.signed_einvoice) -%}
 	<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
 		{% if letter_head and not no_letterhead %}
 			<div class="letter-head">{{ letter_head }}</div>
@@ -170,4 +171,10 @@
 			</tbody>
 		</table>
 	</div>
+	{% else %}
+	<div class="text-center" style="color: var(--gray-500); font-size: 14px;">
+		You must generate IRN before you can preview GST E-Invoice.
+	</div>
+	{% endif %}
 </div>
+
diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py
index 7b1e979..07552e3 100644
--- a/erpnext/accounts/report/balance_sheet/balance_sheet.py
+++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py
@@ -201,17 +201,17 @@
 			net_provisional_profit_loss += provisional_profit_loss.get(key)
 
 	return [
-		{"value": net_asset, "label": "Total Asset", "datatype": "Currency", "currency": currency},
+		{"value": net_asset, "label": _("Total Asset"), "datatype": "Currency", "currency": currency},
 		{
 			"value": net_liability,
-			"label": "Total Liability",
+			"label": _("Total Liability"),
 			"datatype": "Currency",
 			"currency": currency,
 		},
-		{"value": net_equity, "label": "Total Equity", "datatype": "Currency", "currency": currency},
+		{"value": net_equity, "label": _("Total Equity"), "datatype": "Currency", "currency": currency},
 		{
 			"value": net_provisional_profit_loss,
-			"label": "Provisional Profit / Loss (Credit)",
+			"label": _("Provisional Profit / Loss (Credit)"),
 			"indicator": "Green" if net_provisional_profit_loss > 0 else "Red",
 			"datatype": "Currency",
 			"currency": currency,
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
index ca341f4..7b774ba 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
@@ -97,8 +97,8 @@
 			if filters["period"] == "Yearly":
 				labels = [
 					_("Budget") + " " + str(year[0]),
-					_("Actual ") + " " + str(year[0]),
-					_("Variance ") + " " + str(year[0]),
+					_("Actual") + " " + str(year[0]),
+					_("Variance") + " " + str(year[0]),
 				]
 				for label in labels:
 					columns.append(
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
index 8e8465c..ecad9f1 100644
--- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
@@ -230,7 +230,7 @@
 	columns.append(
 		{
 			"fieldname": "total",
-			"label": "Total",
+			"label": _("Total"),
 			"fieldtype": "Currency",
 			"options": "currency",
 			"width": 150,
diff --git a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py
index 8db72de..1a00399 100644
--- a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py
+++ b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py
@@ -29,7 +29,7 @@
 			"options": "Item Group",
 			"width": 150,
 		},
-		{"fieldname": "item", "fieldtype": "Link", "options": "Item", "label": "Item", "width": 150},
+		{"fieldname": "item", "fieldtype": "Link", "options": "Item", "label": _("Item"), "width": 150},
 		{"fieldname": "item_name", "fieldtype": "Data", "label": _("Item Name"), "width": 150},
 		{
 			"fieldname": "customer",
diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py
index 00f5948..3f178f4 100644
--- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py
+++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py
@@ -115,9 +115,9 @@
 		{"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140},
 		{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
 		{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
-		{"fieldname": "range1", "label": "0-30", "fieldtype": "Currency", "width": 140},
-		{"fieldname": "range2", "label": "30-60", "fieldtype": "Currency", "width": 140},
-		{"fieldname": "range3", "label": "60-90", "fieldtype": "Currency", "width": 140},
+		{"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
+		{"fieldname": "range2", "label": _("30-60"), "fieldtype": "Currency", "width": 140},
+		{"fieldname": "range3", "label": _("60-90"), "fieldtype": "Currency", "width": 140},
 		{"fieldname": "range4", "label": _("90 Above"), "fieldtype": "Currency", "width": 140},
 		{
 			"fieldname": "delay_in_payment",
diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py
index c359959..a73c72c 100644
--- a/erpnext/accounts/report/purchase_register/purchase_register.py
+++ b/erpnext/accounts/report/purchase_register/purchase_register.py
@@ -124,11 +124,10 @@
 		_("Purchase Receipt") + ":Link/Purchase Receipt:100",
 		{"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80},
 	]
-	expense_accounts = (
-		tax_accounts
-	) = (
-		expense_columns
-	) = tax_columns = unrealized_profit_loss_accounts = unrealized_profit_loss_account_columns = []
+
+	expense_accounts = []
+	tax_accounts = []
+	unrealized_profit_loss_accounts = []
 
 	if invoice_list:
 		expense_accounts = frappe.db.sql_list(
@@ -163,10 +162,11 @@
 	unrealized_profit_loss_account_columns = [
 		(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts
 	]
-
-	for account in tax_accounts:
-		if account not in expense_accounts:
-			tax_columns.append(account + ":Currency/currency:120")
+	tax_columns = [
+		(account + ":Currency/currency:120")
+		for account in tax_accounts
+		if account not in expense_accounts
+	]
 
 	columns = (
 		columns
diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.json b/erpnext/accounts/report/tax_detail/test_tax_detail.json
index 3a4b175..e490316 100644
--- a/erpnext/accounts/report/tax_detail/test_tax_detail.json
+++ b/erpnext/accounts/report/tax_detail/test_tax_detail.json
@@ -302,7 +302,7 @@
   "is_opening": "No",
   "is_paid": 0,
   "is_return": 0,
-  "is_subcontracted": "No",
+  "is_subcontracted": 0,
   "items": [
    {
     "allow_zero_valuation_rate": 0,
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index f681b34..e759ad0 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -68,7 +68,7 @@
 	def test_item_exists(self):
 		asset = create_asset(item_code="MacBook", do_not_save=1)
 
-		self.assertRaises(frappe.DoesNotExistError, asset.save)
+		self.assertRaises(frappe.ValidationError, asset.save)
 
 	def test_validate_item(self):
 		asset = create_asset(item_code="MacBook Pro", do_not_save=1)
diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py
index 7291daf..a4d2c82 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.py
+++ b/erpnext/assets/doctype/asset_category/asset_category.py
@@ -87,7 +87,7 @@
 					missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name))
 
 			if missing_cwip_accounts_for_company:
-				msg = _("""To enable Capital Work in Progress Accounting, """)
+				msg = _("""To enable Capital Work in Progress Accounting,""") + " "
 				msg += _("""you must select Capital Work in Progress Account in accounts table""")
 				msg += "<br><br>"
 				msg += _("You can also set default CWIP account in Company {}").format(
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py
index e61efad..143f215 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.py
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.py
@@ -46,10 +46,9 @@
 				if d.target_location:
 					frappe.throw(
 						_(
-							"Issuing cannot be done to a location. \
-						Please enter employee who has issued Asset {0}"
+							"Issuing cannot be done to a location. Please enter employee who has issued Asset {0}"
 						).format(d.asset),
-						title="Incorrect Movement Purpose",
+						title=_("Incorrect Movement Purpose"),
 					)
 				if not d.to_employee:
 					frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
@@ -58,10 +57,9 @@
 				if d.to_employee:
 					frappe.throw(
 						_(
-							"Transferring cannot be done to an Employee. \
-						Please enter location where Asset {0} has to be transferred"
+							"Transferring cannot be done to an Employee. Please enter location where Asset {0} has to be transferred"
 						).format(d.asset),
-						title="Incorrect Movement Purpose",
+						title=_("Incorrect Movement Purpose"),
 					)
 				if not d.target_location:
 					frappe.throw(_("Target Location is required while transferring Asset {0}").format(d.asset))
@@ -89,8 +87,7 @@
 					if d.to_employee and d.target_location:
 						frappe.throw(
 							_(
-								"Asset {0} cannot be received at a location and \
-							given to employee in a single movement"
+								"Asset {0} cannot be received at a location and given to employee in a single movement"
 							).format(d.asset)
 						)
 
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 3fe6b2d..f5e4e72 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -32,7 +32,7 @@
 
 	refresh: function(frm) {
 		if (frm.doc.docstatus) {
-			frm.add_custom_button("View General Ledger", function() {
+			frm.add_custom_button(__("View General Ledger"), function() {
 				frappe.route_options = {
 					"voucher_no": frm.doc.name
 				};
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 9953c61..20865e8 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -37,7 +37,7 @@
 				_("Asset Value Adjustment cannot be posted before Asset's purchase date <b>{0}</b>.").format(
 					formatdate(asset_purchase_date)
 				),
-				title="Incorrect Date",
+				title=_("Incorrect Date"),
 			)
 
 	def set_difference_amount(self):
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 2005dac..c9e6798 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -179,7 +179,7 @@
 				if (doc.status != "On Hold") {
 					if(flt(doc.per_received) < 100 && allow_receipt) {
 						cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create'));
-						if(doc.is_subcontracted==="Yes" && me.has_unsupplied_items()) {
+						if(doc.is_subcontracted && me.has_unsupplied_items()) {
 							cur_frm.add_custom_button(__('Material to Supplier'),
 								function() { me.make_stock_entry(); }, __("Transfer"));
 						}
@@ -636,7 +636,7 @@
 frappe.provide("erpnext.buying");
 
 frappe.ui.form.on("Purchase Order", "is_subcontracted", function(frm) {
-	if (frm.doc.is_subcontracted === "Yes") {
+	if (frm.doc.is_subcontracted) {
 		erpnext.buying.get_default_bom(frm);
 	}
 });
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index 896208f..9a1f9d1 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -457,16 +457,15 @@
    "fieldtype": "Column Break"
   },
   {
-   "default": "No",
+   "default": "0",
    "fieldname": "is_subcontracted",
-   "fieldtype": "Select",
+   "fieldtype": "Check",
    "in_standard_filter": 1,
-   "label": "Supply Raw Materials",
-   "options": "No\nYes",
+   "label": "Is Subcontracted",
    "print_hide": 1
   },
   {
-   "depends_on": "eval:doc.is_subcontracted==\"Yes\"",
+   "depends_on": "eval:doc.is_subcontracted",
    "fieldname": "supplier_warehouse",
    "fieldtype": "Link",
    "label": "Supplier Warehouse",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 582bd8d..5860c4c 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -194,7 +194,7 @@
 				)
 
 	def validate_bom_for_subcontracting_items(self):
-		if self.is_subcontracted == "Yes":
+		if self.is_subcontracted:
 			for item in self.items:
 				if not item.bom:
 					frappe.throw(
@@ -294,7 +294,7 @@
 		self.set_status(update=True, status=status)
 		self.update_requested_qty()
 		self.update_ordered_qty()
-		if self.is_subcontracted == "Yes":
+		if self.is_subcontracted:
 			self.update_reserved_qty_for_subcontract()
 
 		self.notify_update()
@@ -311,7 +311,7 @@
 		self.update_ordered_qty()
 		self.validate_budget()
 
-		if self.is_subcontracted == "Yes":
+		if self.is_subcontracted:
 			self.update_reserved_qty_for_subcontract()
 
 		frappe.get_doc("Authorization Control").validate_approving_authority(
@@ -331,7 +331,7 @@
 		if self.has_drop_ship_item():
 			self.update_delivered_qty_in_sales_order()
 
-		if self.is_subcontracted == "Yes":
+		if self.is_subcontracted:
 			self.update_reserved_qty_for_subcontract()
 
 		self.check_on_hold_or_closed_status()
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index e4fb970..1a7f2dd 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -390,7 +390,7 @@
 		frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete()
 
 	def test_update_child_uom_conv_factor_change(self):
-		po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
+		po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
 		total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")])
 
 		trans_item = json.dumps(
@@ -573,7 +573,7 @@
 		automatically_fetch_payment_terms(enable=0)
 
 	def test_subcontracting(self):
-		po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
+		po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
 		self.assertEqual(len(po.get("supplied_items")), 2)
 
 	def test_warehouse_company_validation(self):
@@ -617,7 +617,7 @@
 				"doctype": "Purchase Order",
 				"company": "_Test Company",
 				"supplier": "_Test Supplier",
-				"is_subcontracted": "No",
+				"is_subcontracted": 0,
 				"schedule_date": add_days(nowdate(), 1),
 				"currency": frappe.get_cached_value("Company", "_Test Company", "default_currency"),
 				"conversion_factor": 1,
@@ -764,7 +764,7 @@
 		)
 
 		# Submit PO
-		po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
+		po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
 
 		bin2 = frappe.db.get_value(
 			"Bin",
@@ -919,7 +919,7 @@
 		po = create_purchase_order(
 			item_code=item_code,
 			qty=1,
-			is_subcontracted="Yes",
+			is_subcontracted=1,
 			supplier_warehouse="_Test Warehouse 1 - _TC",
 			include_exploded_items=1,
 		)
@@ -936,7 +936,7 @@
 		po1 = create_purchase_order(
 			item_code=item_code,
 			qty=1,
-			is_subcontracted="Yes",
+			is_subcontracted=1,
 			supplier_warehouse="_Test Warehouse 1 - _TC",
 			include_exploded_items=0,
 		)
@@ -957,7 +957,7 @@
 		po = create_purchase_order(
 			item_code=item_code,
 			qty=order_qty,
-			is_subcontracted="Yes",
+			is_subcontracted=1,
 			supplier_warehouse="_Test Warehouse 1 - _TC",
 		)
 
@@ -1050,7 +1050,7 @@
 		po = create_purchase_order(
 			item_code=item_code,
 			qty=order_qty,
-			is_subcontracted="Yes",
+			is_subcontracted=1,
 			supplier_warehouse="_Test Warehouse 1 - _TC",
 			do_not_save=True,
 		)
@@ -1283,7 +1283,7 @@
 	po.schedule_date = add_days(nowdate(), 1)
 	po.company = args.company or "_Test Company"
 	po.supplier = args.supplier or "_Test Supplier"
-	po.is_subcontracted = args.is_subcontracted or "No"
+	po.is_subcontracted = args.is_subcontracted or 0
 	po.currency = args.currency or frappe.get_cached_value("Company", po.company, "default_currency")
 	po.conversion_factor = args.conversion_factor or 1
 	po.supplier_warehouse = args.supplier_warehouse or None
@@ -1309,7 +1309,7 @@
 	if not args.do_not_save:
 		po.insert()
 		if not args.do_not_submit:
-			if po.is_subcontracted == "Yes":
+			if po.is_subcontracted:
 				supp_items = po.get("supplied_items")
 				for d in supp_items:
 					if not d.reserve_warehouse:
diff --git a/erpnext/buying/doctype/purchase_order/test_records.json b/erpnext/buying/doctype/purchase_order/test_records.json
index 74b8f1b..896050c 100644
--- a/erpnext/buying/doctype/purchase_order/test_records.json
+++ b/erpnext/buying/doctype/purchase_order/test_records.json
@@ -8,7 +8,7 @@
   "doctype": "Purchase Order", 
   "base_grand_total": 5000.0, 
   "grand_total": 5000.0, 
-  "is_subcontracted": "Yes", 
+  "is_subcontracted": 1, 
   "naming_series": "_T-Purchase Order-", 
   "base_net_total": 5000.0, 
   "items": [
@@ -42,7 +42,7 @@
   "doctype": "Purchase Order", 
   "base_grand_total": 5000.0, 
   "grand_total": 5000.0, 
-  "is_subcontracted": "No", 
+  "is_subcontracted": 0, 
   "naming_series": "_T-Purchase Order-", 
   "base_net_total": 5000.0, 
   "items": [
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index a18c527..f72c598 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -572,7 +572,7 @@
    "read_only": 1
   },
   {
-   "depends_on": "eval:parent.is_subcontracted == 'Yes'",
+   "depends_on": "eval:parent.is_subcontracted",
    "fieldname": "bom",
    "fieldtype": "Link",
    "label": "BOM",
@@ -581,7 +581,7 @@
   },
   {
    "default": "0",
-   "depends_on": "eval:parent.is_subcontracted == 'Yes'",
+   "depends_on": "eval:parent.is_subcontracted",
    "fieldname": "include_exploded_items",
    "fieldtype": "Check",
    "label": "Include Exploded Items",
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
index 567e41f..8d1939a 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
@@ -773,11 +773,10 @@
    "fieldtype": "Column Break"
   },
   {
-   "default": "No",
+   "default": "0",
    "fieldname": "is_subcontracted",
-   "fieldtype": "Select",
+   "fieldtype": "Check",
    "label": "Is Subcontracted",
-   "options": "\nYes\nNo",
    "print_hide": 1
   },
   {
diff --git a/erpnext/buying/doctype/supplier_quotation/test_records.json b/erpnext/buying/doctype/supplier_quotation/test_records.json
index 0f835d2..8acac32 100644
--- a/erpnext/buying/doctype/supplier_quotation/test_records.json
+++ b/erpnext/buying/doctype/supplier_quotation/test_records.json
@@ -7,7 +7,7 @@
   "doctype": "Supplier Quotation", 
   "base_grand_total": 5000.0, 
   "grand_total": 5000.0, 
-  "is_subcontracted": "No", 
+  "is_subcontracted": 0, 
   "naming_series": "_T-Supplier Quotation-", 
   "base_net_total": 5000.0, 
   "items": [
diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
index 992bc80..486bf23 100644
--- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
+++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
@@ -213,7 +213,8 @@
 		end_date = get_scorecard_date(sc.period, start_date)
 	if scp_count > 0:
 		frappe.msgprint(
-			_("Created {0} scorecards for {1} between: ").format(scp_count, sc.supplier)
+			_("Created {0} scorecards for {1} between:").format(scp_count, sc.supplier)
+			+ " "
 			+ str(first_start_date)
 			+ " - "
 			+ str(last_end_date)
diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py
index 130adc9..ab7d487 100644
--- a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py
+++ b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py
@@ -80,6 +80,6 @@
 				)[0]
 				my_variables.append(var)
 			except Exception:
-				frappe.throw(_("Unable to find variable: ") + str(match.group(1)), InvalidFormulaVariable)
+				frappe.throw(_("Unable to find variable:") + " " + str(match.group(1)), InvalidFormulaVariable)
 
 	return my_variables
diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py
index 11a7449..dbdc62e 100644
--- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py
+++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py
@@ -48,7 +48,7 @@
 		"data": {
 			"labels": labels,
 			"datasets": [
-				{"name": _("{0}").format(filters.get("period")) + _(" Purchase Value"), "values": datapoints}
+				{"name": _(filters.get("period")) + " " + _("Purchase Value"), "values": datapoints}
 			],
 		},
 		"type": "line",
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
index 5ba52f1..6889322 100644
--- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
@@ -35,7 +35,7 @@
 				return {
 					filters: {
 						docstatus: 1,
-						is_subcontracted: 'Yes',
+						is_subcontracted: 1,
 						company: frappe.query_report.get_filter_value('company')
 					}
 				}
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
index 1b2705a..3d66637 100644
--- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
@@ -45,7 +45,7 @@
 def get_filters(report_filters):
 	filters = [
 		["Purchase Order", "docstatus", "=", 1],
-		["Purchase Order", "is_subcontracted", "=", "Yes"],
+		["Purchase Order", "is_subcontracted", "=", 1],
 		[
 			"Purchase Order",
 			"transaction_date",
diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py
index 004657b..2e90de6 100644
--- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py
+++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py
@@ -78,7 +78,7 @@
 
 def get_po(filters):
 	record_filters = [
-		["is_subcontracted", "=", "Yes"],
+		["is_subcontracted", "=", 1],
 		["supplier", "=", filters.supplier],
 		["transaction_date", "<=", filters.to_date],
 		["transaction_date", ">=", filters.from_date],
diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py
index 26e4243..57f8741 100644
--- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py
+++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py
@@ -17,7 +17,7 @@
 
 class TestSubcontractedItemToBeReceived(FrappeTestCase):
 	def test_pending_and_received_qty(self):
-		po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
+		po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
 		transfer_param = []
 		make_stock_entry(
 			item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100
diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py
index 98b18da..6b8a3b1 100644
--- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py
+++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py
@@ -72,7 +72,7 @@
 		],
 		filters=[
 			["Purchase Order", "per_received", "<", "100"],
-			["Purchase Order", "is_subcontracted", "=", "Yes"],
+			["Purchase Order", "is_subcontracted", "=", 1],
 			["Purchase Order", "supplier", "=", filters.supplier],
 			["Purchase Order", "transaction_date", "<=", filters.to_date],
 			["Purchase Order", "transaction_date", ">=", filters.from_date],
diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py
index 401176d..2791a26 100644
--- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py
+++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py
@@ -19,7 +19,7 @@
 class TestSubcontractedItemToBeTransferred(FrappeTestCase):
 	def test_pending_and_transferred_qty(self):
 		po = create_purchase_order(
-			item_code="_Test FG Item", is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			item_code="_Test FG Item", is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		# Material Receipt of RMs
diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py
index f97cd5e..e904af0 100644
--- a/erpnext/buying/utils.py
+++ b/erpnext/buying/utils.py
@@ -3,19 +3,19 @@
 
 
 import json
+from typing import Dict
 
 import frappe
 from frappe import _
-from frappe.utils import cint, cstr, flt
+from frappe.utils import cint, cstr, flt, getdate
 
 from erpnext.stock.doctype.item.item import get_last_purchase_details, validate_end_of_life
 
 
-def update_last_purchase_rate(doc, is_submit):
+def update_last_purchase_rate(doc, is_submit) -> None:
 	"""updates last_purchase_rate in item table for each item"""
-	import frappe.utils
 
-	this_purchase_date = frappe.utils.getdate(doc.get("posting_date") or doc.get("transaction_date"))
+	this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
 
 	for d in doc.get("items"):
 		# get last purchase details
@@ -41,7 +41,7 @@
 		frappe.db.set_value("Item", d.item_code, "last_purchase_rate", flt(last_purchase_rate))
 
 
-def validate_for_items(doc):
+def validate_for_items(doc) -> None:
 	items = []
 	for d in doc.get("items"):
 		if not d.qty:
@@ -49,40 +49,11 @@
 				continue
 			frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code))
 
-		# update with latest quantities
-		bin = frappe.db.sql(
-			"""select projected_qty from `tabBin` where
-			item_code = %s and warehouse = %s""",
-			(d.item_code, d.warehouse),
-			as_dict=1,
-		)
-
-		f_lst = {
-			"projected_qty": bin and flt(bin[0]["projected_qty"]) or 0,
-			"ordered_qty": 0,
-			"received_qty": 0,
-		}
-		if d.doctype in ("Purchase Receipt Item", "Purchase Invoice Item"):
-			f_lst.pop("received_qty")
-		for x in f_lst:
-			if d.meta.get_field(x):
-				d.set(x, f_lst[x])
-
-		item = frappe.db.sql(
-			"""select is_stock_item,
-			is_sub_contracted_item, end_of_life, disabled from `tabItem` where name=%s""",
-			d.item_code,
-			as_dict=1,
-		)[0]
-
+		set_stock_levels(row=d)  # update with latest quantities
+		item = validate_item_and_get_basic_data(row=d)
+		validate_stock_item_warehouse(row=d, item=item)
 		validate_end_of_life(d.item_code, item.end_of_life, item.disabled)
 
-		# validate stock item
-		if item.is_stock_item == 1 and d.qty and not d.warehouse and not d.get("delivered_by_supplier"):
-			frappe.throw(
-				_("Warehouse is mandatory for stock Item {0} in row {1}").format(d.item_code, d.idx)
-			)
-
 		items.append(cstr(d.item_code))
 
 	if (
@@ -93,7 +64,57 @@
 		frappe.throw(_("Same item cannot be entered multiple times."))
 
 
-def check_on_hold_or_closed_status(doctype, docname):
+def set_stock_levels(row) -> None:
+	projected_qty = frappe.db.get_value(
+		"Bin",
+		{
+			"item_code": row.item_code,
+			"warehouse": row.warehouse,
+		},
+		"projected_qty",
+	)
+
+	qty_data = {
+		"projected_qty": flt(projected_qty),
+		"ordered_qty": 0,
+		"received_qty": 0,
+	}
+	if row.doctype in ("Purchase Receipt Item", "Purchase Invoice Item"):
+		qty_data.pop("received_qty")
+
+	for field in qty_data:
+		if row.meta.get_field(field):
+			row.set(field, qty_data[field])
+
+
+def validate_item_and_get_basic_data(row) -> Dict:
+	item = frappe.db.get_values(
+		"Item",
+		filters={"name": row.item_code},
+		fieldname=["is_stock_item", "is_sub_contracted_item", "end_of_life", "disabled"],
+		as_dict=1,
+	)
+	if not item:
+		frappe.throw(_("Row #{0}: Item {1} does not exist").format(row.idx, frappe.bold(row.item_code)))
+
+	return item[0]
+
+
+def validate_stock_item_warehouse(row, item) -> None:
+	if (
+		item.is_stock_item == 1
+		and row.qty
+		and not row.warehouse
+		and not row.get("delivered_by_supplier")
+	):
+		frappe.throw(
+			_("Row #{1}: Warehouse is mandatory for stock Item {0}").format(
+				frappe.bold(row.item_code), row.idx
+			)
+		)
+
+
+def check_on_hold_or_closed_status(doctype, docname) -> None:
 	status = frappe.db.get_value(doctype, docname, "status")
 
 	if status in ("Closed", "On Hold"):
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 72ac1b3..8a9318e 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1267,17 +1267,9 @@
 		stock_items = []
 		item_codes = list(set(item.item_code for item in self.get("items")))
 		if item_codes:
-			stock_items = [
-				r[0]
-				for r in frappe.db.sql(
-					"""
-				select name from `tabItem`
-				where name in (%s) and is_stock_item=1
-			"""
-					% (", ".join(["%s"] * len(item_codes)),),
-					item_codes,
-				)
-			]
+			stock_items = frappe.db.get_values(
+				"Item", {"name": ["in", item_codes], "is_stock_item": 1}, pluck="name", cache=True
+			)
 
 		return stock_items
 
@@ -2594,7 +2586,7 @@
 		parent.update_ordered_qty()
 		parent.update_ordered_and_reserved_qty()
 		parent.update_receiving_percentage()
-		if parent.is_subcontracted == "Yes":
+		if parent.is_subcontracted:
 			parent.update_reserved_qty_for_subcontract()
 			parent.create_raw_materials_supplied("supplied_items")
 			parent.save()
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 4789207..eda3686 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -167,7 +167,7 @@
 					_("Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same").format(item.idx)
 				)
 
-			if item.get("from_warehouse") and self.get("is_subcontracted") == "Yes":
+			if item.get("from_warehouse") and self.get("is_subcontracted"):
 				frappe.throw(
 					_(
 						"Row #{0}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor"
@@ -339,10 +339,7 @@
 		return supplied_items_cost
 
 	def validate_for_subcontracting(self):
-		if not self.is_subcontracted and self.sub_contracted_items:
-			frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No"))
-
-		if self.is_subcontracted == "Yes":
+		if self.is_subcontracted:
 			if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
 				frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype))
 
@@ -363,14 +360,14 @@
 					item.bom = None
 
 	def create_raw_materials_supplied(self, raw_material_table):
-		if self.is_subcontracted == "Yes":
+		if self.is_subcontracted:
 			self.set_materials_for_subcontracted_items(raw_material_table)
 
 		elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
 			for item in self.get("items"):
 				item.rm_supp_cost = 0.0
 
-		if self.is_subcontracted == "No" and self.get("supplied_items"):
+		if not self.is_subcontracted and self.get("supplied_items"):
 			self.set("supplied_items", [])
 
 	@property
@@ -466,7 +463,10 @@
 		stock_items = self.get_stock_items()
 
 		for d in self.get("items"):
-			if d.item_code in stock_items and d.warehouse:
+			if d.item_code not in stock_items:
+				continue
+
+			if d.warehouse:
 				pr_qty = flt(d.qty) * flt(d.conversion_factor)
 
 				if pr_qty:
@@ -491,6 +491,7 @@
 					sle = self.get_sl_entries(
 						d, {"actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip()}
 					)
+
 					if self.is_return:
 						outgoing_rate = get_rate_for_return(
 							self.doctype, self.name, d.item_code, self.return_against, item_row=d
@@ -520,18 +521,18 @@
 
 						sl_entries.append(from_warehouse_sle)
 
-				if flt(d.rejected_qty) != 0:
-					sl_entries.append(
-						self.get_sl_entries(
-							d,
-							{
-								"warehouse": d.rejected_warehouse,
-								"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
-								"serial_no": cstr(d.rejected_serial_no).strip(),
-								"incoming_rate": 0.0,
-							},
-						)
+			if flt(d.rejected_qty) != 0:
+				sl_entries.append(
+					self.get_sl_entries(
+						d,
+						{
+							"warehouse": d.rejected_warehouse,
+							"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
+							"serial_no": cstr(d.rejected_serial_no).strip(),
+							"incoming_rate": 0.0,
+						},
 					)
+				)
 
 		self.make_sl_entries_for_supplier_warehouse(sl_entries)
 		self.make_sl_entries(
@@ -803,7 +804,7 @@
 		if self.doctype == "Material Request":
 			return
 
-		if hasattr(self, "is_subcontracted") and self.is_subcontracted == "Yes":
+		if hasattr(self, "is_subcontracted") and self.is_subcontracted:
 			validate_item_type(self, "is_sub_contracted_item", "subcontracted")
 		else:
 			validate_item_type(self, "is_purchase_item", "purchase")
diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py
index 7083088..4bce06f 100644
--- a/erpnext/controllers/subcontracting.py
+++ b/erpnext/controllers/subcontracting.py
@@ -407,7 +407,7 @@
 
 	def set_consumed_qty_in_po(self):
 		# Update consumed qty back in the purchase order
-		if self.is_subcontracted != "Yes":
+		if not self.is_subcontracted:
 			return
 
 		self.__get_purchase_orders()
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
index 7aa0b77..d532236 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
@@ -37,7 +37,7 @@
 			let msg,color;
 
 			if (days>0){
-				msg = __("Your Session will be expire in ") + days + __(" days.");
+				msg = __("Your Session will be expire in {0} days.", [days]);
 				color = "green";
 			}
 			else {
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 03ff269..96c730c 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -126,7 +126,8 @@
 	def declare_enquiry_lost(self, lost_reasons_list, competitors, detailed_reason=None):
 		if not self.has_active_quotation():
 			self.status = "Lost"
-			self.lost_reasons = self.competitors = []
+			self.lost_reasons = []
+			self.competitors = []
 
 			if detailed_reason:
 				self.order_lost_reason = detailed_reason
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js
index 6874caa..d4ac0ba 100644
--- a/erpnext/crm/doctype/social_media_post/social_media_post.js
+++ b/erpnext/crm/doctype/social_media_post/social_media_post.js
@@ -86,7 +86,7 @@
 				frm.trigger('add_post_btn');
 			}
 			if (frm.doc.post_status !='Deleted') {
-				frm.add_custom_button(('Delete Post'), function() {
+				frm.add_custom_button(__('Delete Post'), function() {
 					frappe.confirm(__('Are you sure want to delete the Post from Social Media platforms?'),
 						function() {
 							frappe.call({
diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py
index 9dae1d5..db36581 100644
--- a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py
+++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py
@@ -3,11 +3,12 @@
 
 
 import frappe
+from frappe import _
 
 
 def execute(filters=None):
 	columns = [
-		{"fieldname": "creation_date", "label": "Date", "fieldtype": "Date", "width": 300},
+		{"fieldname": "creation_date", "label": _("Date"), "fieldtype": "Date", "width": 300},
 		{
 			"fieldname": "first_response_time",
 			"fieldtype": "Duration",
diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py
index 77e6ae2..3a46fb0 100644
--- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py
+++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py
@@ -1,9 +1,9 @@
 # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
 import json
+from itertools import groupby
 
 import frappe
-import pandas
 from frappe import _
 from frappe.utils import flt
 
@@ -101,18 +101,19 @@
 
 			self.convert_to_base_currency()
 
-			dataframe = pandas.DataFrame.from_records(self.query_result)
-			dataframe.replace(to_replace=[None], value="Not Assigned", inplace=True)
-			result = dataframe.groupby(["sales_stage", based_on], as_index=False)["amount"].sum()
+			for row in self.query_result:
+				if not row.get(based_on):
+					row[based_on] = "Not Assigned"
 
 			self.grouped_data = []
 
-			for i in range(len(result["amount"])):
+			grouping_key = lambda o: (o["sales_stage"], o[based_on])  # noqa
+			for (sales_stage, _based_on), rows in groupby(self.query_result, grouping_key):
 				self.grouped_data.append(
 					{
-						"sales_stage": result["sales_stage"][i],
-						based_on: result[based_on][i],
-						"amount": result["amount"][i],
+						"sales_stage": sales_stage,
+						based_on: _based_on,
+						"amount": sum(flt(r["amount"]) for r in rows),
 					}
 				)
 
diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py
index b0c174b..d23a22a 100644
--- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py
+++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py
@@ -3,9 +3,9 @@
 
 import json
 from datetime import date
+from itertools import groupby
 
 import frappe
-import pandas
 from dateutil.relativedelta import relativedelta
 from frappe import _
 from frappe.utils import cint, flt
@@ -109,18 +109,15 @@
 
 			self.convert_to_base_currency()
 
-			dataframe = pandas.DataFrame.from_records(self.query_result)
-			dataframe.replace(to_replace=[None], value="Not Assigned", inplace=True)
-			result = dataframe.groupby([self.pipeline_by, self.period_by], as_index=False)["amount"].sum()
-
 			self.grouped_data = []
 
-			for i in range(len(result["amount"])):
+			grouping_key = lambda o: (o.get(self.pipeline_by) or "Not Assigned", o[self.period_by])  # noqa
+			for (pipeline_by, period_by), rows in groupby(self.query_result, grouping_key):
 				self.grouped_data.append(
 					{
-						self.pipeline_by: result[self.pipeline_by][i],
-						self.period_by: result[self.period_by][i],
-						"amount": result["amount"][i],
+						self.pipeline_by: pipeline_by,
+						self.period_by: period_by,
+						"amount": sum(flt(r["amount"]) for r in rows),
 					}
 				)
 
diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json
index d5fb969..e6f08f7 100644
--- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json
@@ -47,7 +47,7 @@
   "item_search_settings_section",
   "redisearch_warning",
   "search_index_fields",
-  "show_categories_in_search_autocomplete",
+  "is_redisearch_enabled",
   "is_redisearch_loaded",
   "shop_by_category_section",
   "slideshow",
@@ -293,6 +293,7 @@
    "fieldname": "search_index_fields",
    "fieldtype": "Small Text",
    "label": "Search Index Fields",
+   "mandatory_depends_on": "is_redisearch_enabled",
    "read_only_depends_on": "eval:!doc.is_redisearch_loaded"
   },
   {
@@ -302,13 +303,6 @@
    "label": "Item Search Settings"
   },
   {
-   "default": "1",
-   "fieldname": "show_categories_in_search_autocomplete",
-   "fieldtype": "Check",
-   "label": "Show Categories in Search Autocomplete",
-   "read_only_depends_on": "eval:!doc.is_redisearch_loaded"
-  },
-  {
    "default": "0",
    "fieldname": "is_redisearch_loaded",
    "fieldtype": "Check",
@@ -365,12 +359,19 @@
    "fieldname": "show_price_in_quotation",
    "fieldtype": "Check",
    "label": "Show Price in Quotation"
+  },
+  {
+   "default": "0",
+   "fieldname": "is_redisearch_enabled",
+   "fieldtype": "Check",
+   "label": "Enable Redisearch",
+   "read_only_depends_on": "eval:!doc.is_redisearch_loaded"
   }
  ],
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2021-09-02 14:02:44.785824",
+ "modified": "2022-04-01 18:35:56.106756",
  "modified_by": "Administrator",
  "module": "E-commerce",
  "name": "E Commerce Settings",
@@ -389,5 +390,6 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
index b5cd067..f85667e 100644
--- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
@@ -9,6 +9,7 @@
 
 from erpnext.e_commerce.redisearch_utils import (
 	create_website_items_index,
+	define_autocomplete_dictionary,
 	get_indexable_web_fields,
 	is_search_module_loaded,
 )
@@ -21,6 +22,8 @@
 class ECommerceSettings(Document):
 	def onload(self):
 		self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
+
+		# flag >> if redisearch is installed and loaded
 		self.is_redisearch_loaded = is_search_module_loaded()
 
 	def validate(self):
@@ -34,6 +37,20 @@
 
 		frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings")
 
+		self.is_redisearch_enabled_pre_save = frappe.db.get_single_value(
+			"E Commerce Settings", "is_redisearch_enabled"
+		)
+
+	def after_save(self):
+		self.create_redisearch_indexes()
+
+	def create_redisearch_indexes(self):
+		# if redisearch is enabled (value changed) create indexes and dictionary
+		value_changed = self.is_redisearch_enabled != self.is_redisearch_enabled_pre_save
+		if self.is_redisearch_loaded and self.is_redisearch_enabled and value_changed:
+			define_autocomplete_dictionary()
+			create_website_items_index()
+
 	def validate_field_filters(self):
 		if not (self.enable_field_filters and self.filter_fields):
 			return
@@ -146,12 +163,7 @@
 
 
 def get_shopping_cart_settings():
-	if not getattr(frappe.local, "shopping_cart_settings", None):
-		frappe.local.shopping_cart_settings = frappe.get_doc(
-			"E Commerce Settings", "E Commerce Settings"
-		)
-
-	return frappe.local.shopping_cart_settings
+	return frappe.get_cached_doc("E Commerce Settings")
 
 
 @frappe.whitelist(allow_guest=True)
diff --git a/erpnext/e_commerce/doctype/website_item/website_item.js b/erpnext/e_commerce/doctype/website_item/website_item.js
index 7108cab..7295e4b 100644
--- a/erpnext/e_commerce/doctype/website_item/website_item.js
+++ b/erpnext/e_commerce/doctype/website_item/website_item.js
@@ -2,7 +2,7 @@
 // For license information, please see license.txt
 
 frappe.ui.form.on('Website Item', {
-	onload: function(frm) {
+	onload: (frm) => {
 		// should never check Private
 		frm.fields_dict["website_image"].df.is_private = 0;
 
@@ -13,18 +13,35 @@
 		});
 	},
 
-	image: function() {
+	refresh: (frm) => {
+		frm.add_custom_button(__("Prices"), function() {
+			frappe.set_route("List", "Item Price", {"item_code": frm.doc.item_code});
+		}, __("View"));
+
+		frm.add_custom_button(__("Stock"), function() {
+			frappe.route_options = {
+				"item_code": frm.doc.item_code
+			};
+			frappe.set_route("query-report", "Stock Balance");
+		}, __("View"));
+
+		frm.add_custom_button(__("E Commerce Settings"), function() {
+			frappe.set_route("Form", "E Commerce Settings");
+		}, __("View"));
+	},
+
+	image: () => {
 		refresh_field("image_view");
 	},
 
-	copy_from_item_group: function(frm) {
+	copy_from_item_group: (frm) => {
 		return frm.call({
 			doc: frm.doc,
 			method: "copy_specification_from_item_group"
 		});
 	},
 
-	set_meta_tags(frm) {
+	set_meta_tags: (frm) => {
 		frappe.utils.set_meta_tag(frm.doc.route);
 	}
 });
diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py
index 82829bf..f2dd796 100644
--- a/erpnext/e_commerce/redisearch_utils.py
+++ b/erpnext/e_commerce/redisearch_utils.py
@@ -1,8 +1,12 @@
-# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
+import json
+
 import frappe
+from frappe import _
 from frappe.utils.redis_wrapper import RedisWrapper
+from redis import ResponseError
 from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField
 
 WEBSITE_ITEM_INDEX = "website_items_index"
@@ -22,6 +26,12 @@
 	return [df.fieldname for df in valid_fields]
 
 
+def is_redisearch_enabled():
+	"Return True only if redisearch is loaded and enabled."
+	is_redisearch_enabled = frappe.db.get_single_value("E Commerce Settings", "is_redisearch_enabled")
+	return is_search_module_loaded() and is_redisearch_enabled
+
+
 def is_search_module_loaded():
 	try:
 		cache = frappe.cache()
@@ -32,14 +42,14 @@
 		)
 		return "search" in parsed_output
 	except Exception:
-		return False
+		return False  # handling older redis versions
 
 
-def if_redisearch_loaded(function):
-	"Decorator to check if Redisearch is loaded."
+def if_redisearch_enabled(function):
+	"Decorator to check if Redisearch is enabled."
 
 	def wrapper(*args, **kwargs):
-		if is_search_module_loaded():
+		if is_redisearch_enabled():
 			func = function(*args, **kwargs)
 			return func
 		return
@@ -51,22 +61,25 @@
 	return "{0}|{1}".format(frappe.conf.db_name, key).encode("utf-8")
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def create_website_items_index():
 	"Creates Index Definition."
 
 	# CREATE index
 	client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache())
 
-	# DROP if already exists
 	try:
-		client.drop_index()
-	except Exception:
+		client.drop_index()  # drop if already exists
+	except ResponseError:
+		# will most likely raise a ResponseError if index does not exist
+		# ignore and create index
 		pass
+	except Exception:
+		raise_redisearch_error()
 
 	idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
 
-	# Based on e-commerce settings
+	# Index fields mentioned in e-commerce settings
 	idx_fields = frappe.db.get_single_value("E Commerce Settings", "search_index_fields")
 	idx_fields = idx_fields.split(",") if idx_fields else []
 
@@ -91,20 +104,20 @@
 	return TextField(field)
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def insert_item_to_index(website_item_doc):
 	# Insert item to index
 	key = get_cache_key(website_item_doc.name)
 	cache = frappe.cache()
 	web_item = create_web_item_map(website_item_doc)
 
-	for k, v in web_item.items():
-		super(RedisWrapper, cache).hset(make_key(key), k, v)
+	for field, value in web_item.items():
+		super(RedisWrapper, cache).hset(make_key(key), field, value)
 
 	insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def insert_to_name_ac(web_name, doc_name):
 	ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache())
 	ac.add_suggestions(Suggestion(web_name, payload=doc_name))
@@ -114,20 +127,20 @@
 	fields_to_index = get_fields_indexed()
 	web_item = {}
 
-	for f in fields_to_index:
-		web_item[f] = website_item_doc.get(f) or ""
+	for field in fields_to_index:
+		web_item[field] = website_item_doc.get(field) or ""
 
 	return web_item
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def update_index_for_item(website_item_doc):
 	# Reinsert to Cache
 	insert_item_to_index(website_item_doc)
 	define_autocomplete_dictionary()
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def delete_item_from_index(website_item_doc):
 	cache = frappe.cache()
 	key = get_cache_key(website_item_doc.name)
@@ -135,13 +148,13 @@
 	try:
 		cache.delete(key)
 	except Exception:
-		return False
+		raise_redisearch_error()
 
 	delete_from_ac_dict(website_item_doc)
 	return True
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def delete_from_ac_dict(website_item_doc):
 	"""Removes this items's name from autocomplete dictionary"""
 	cache = frappe.cache()
@@ -149,40 +162,60 @@
 	name_ac.delete(website_item_doc.web_item_name)
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
 def define_autocomplete_dictionary():
-	"""Creates an autocomplete search dictionary for `name`.
-	Also creats autocomplete dictionary for `categories` if
-	checked in E Commerce Settings"""
+	"""
+	Defines/Redefines an autocomplete search dictionary for Website Item Name.
+	Also creats autocomplete dictionary for Published Item Groups.
+	"""
 
 	cache = frappe.cache()
-	name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
-	cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
-
-	ac_categories = frappe.db.get_single_value(
-		"E Commerce Settings", "show_categories_in_search_autocomplete"
-	)
+	item_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
+	item_group_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
 
 	# Delete both autocomplete dicts
 	try:
 		cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
 		cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
 	except Exception:
-		return False
+		raise_redisearch_error()
 
+	create_items_autocomplete_dict(autocompleter=item_ac)
+	create_item_groups_autocomplete_dict(autocompleter=item_group_ac)
+
+
+@if_redisearch_enabled
+def create_items_autocomplete_dict(autocompleter):
+	"Add items as suggestions in Autocompleter."
 	items = frappe.get_all(
 		"Website Item", fields=["web_item_name", "item_group"], filters={"published": 1}
 	)
 
 	for item in items:
-		name_ac.add_suggestions(Suggestion(item.web_item_name))
-		if ac_categories and item.item_group:
-			cat_ac.add_suggestions(Suggestion(item.item_group))
-
-	return True
+		autocompleter.add_suggestions(Suggestion(item.web_item_name))
 
 
-@if_redisearch_loaded
+@if_redisearch_enabled
+def create_item_groups_autocomplete_dict(autocompleter):
+	"Add item groups with weightage as suggestions in Autocompleter."
+	published_item_groups = frappe.get_all(
+		"Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1}
+	)
+	if not published_item_groups:
+		return
+
+	for item_group in published_item_groups:
+		payload = json.dumps({"name": item_group.name, "route": item_group.route})
+		autocompleter.add_suggestions(
+			Suggestion(
+				string=item_group.name,
+				score=frappe.utils.flt(item_group.weightage) or 1.0,
+				payload=payload,  # additional info that can be retrieved later
+			)
+		)
+
+
+@if_redisearch_enabled
 def reindex_all_web_items():
 	items = frappe.get_all("Website Item", fields=get_fields_indexed(), filters={"published": True})
 
@@ -191,8 +224,8 @@
 		web_item = create_web_item_map(item)
 		key = make_key(get_cache_key(item.name))
 
-		for k, v in web_item.items():
-			super(RedisWrapper, cache).hset(key, k, v)
+		for field, value in web_item.items():
+			super(RedisWrapper, cache).hset(key, field, value)
 
 
 def get_cache_key(name):
@@ -210,7 +243,12 @@
 	return fields_to_index
 
 
-# TODO: Remove later
-# # Figure out a way to run this at startup
-define_autocomplete_dictionary()
-create_website_items_index()
+def raise_redisearch_error():
+	"Create an Error Log and raise error."
+	traceback = frappe.get_traceback()
+	log = frappe.log_error(traceback, frappe._("Redisearch Error"))
+	log_link = frappe.utils.get_link_to_form("Error Log", log.name)
+
+	frappe.throw(
+		msg=_("Something went wrong. Check {0}").format(log_link), title=_("Redisearch Error")
+	)
diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
index 4db6f98..b3072c2 100644
--- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
+++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
@@ -41,10 +41,8 @@
 			if self.day == calendar.day_name[getdate(date).weekday()]:
 				course_schedule = self.make_course_schedule(date)
 				try:
-					print("pass")
 					course_schedule.save()
 				except OverlapError:
-					print("fail")
 					course_schedules_errors.append(date)
 				else:
 					course_schedules.append(course_schedule)
diff --git a/erpnext/education/doctype/education_settings/education_settings.py b/erpnext/education/doctype/education_settings/education_settings.py
index cde5089..295aa3a 100644
--- a/erpnext/education/doctype/education_settings/education_settings.py
+++ b/erpnext/education/doctype/education_settings/education_settings.py
@@ -41,4 +41,4 @@
 
 
 def update_website_context(context):
-	context["lms_enabled"] = frappe.get_doc("Education Settings").enable_lms
+	context["lms_enabled"] = frappe.get_cached_doc("Education Settings").enable_lms
diff --git a/erpnext/education/doctype/student_admission/templates/student_admission_row.html b/erpnext/education/doctype/student_admission/templates/student_admission_row.html
index 529d651..dc4587b 100644
--- a/erpnext/education/doctype/student_admission/templates/student_admission_row.html
+++ b/erpnext/education/doctype/student_admission/templates/student_admission_row.html
@@ -1,6 +1,6 @@
 <div class="web-list-item transaction-list-item">
 	{% set today = frappe.utils.getdate(frappe.utils.nowdate()) %}
-	<a href = "{{ doc.route }}/" class="no-underline">
+	<a href = "{{ doc.route }}" class="no-underline">
 		<div class="row">
 			<div class="col-sm-4 bold">
 				<span class="indicator
diff --git a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py
index 0fb2550..bbeb654 100644
--- a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py
+++ b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py
@@ -69,13 +69,13 @@
 		l = len(self.courses)
 		for d in self.courses:
 			if not d.student_group_name:
-				frappe.throw(_("""Student Group Name is mandatory in row {0}""".format(d.idx)))
+				frappe.throw(_("Student Group Name is mandatory in row {0}").format(d.idx))
 
 			if d.group_based_on == "Course" and not d.course:
-				frappe.throw(_("""Course is mandatory in row {0}""".format(d.idx)))
+				frappe.throw(_("Course is mandatory in row {0}").format(d.idx))
 
 			if d.group_based_on == "Batch" and not d.batch:
-				frappe.throw(_("""Batch is mandatory in row {0}""".format(d.idx)))
+				frappe.throw(_("Batch is mandatory in row {0}").format(d.idx))
 
 			frappe.publish_realtime(
 				"student_group_creation_progress", {"progress": [d.idx, l]}, user=frappe.session.user
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index a2b1c41..1c009d3 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -469,7 +469,7 @@
 	],
 	"daily_long": [
 		"erpnext.setup.doctype.email_digest.email_digest.send",
-		"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
+		"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
 		"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
 		"erpnext.hr.utils.generate_leave_encashment",
 		"erpnext.hr.utils.allocate_earned_leaves",
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index 7f4bd83..e43d40e 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -5,11 +5,21 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate
+from frappe.query_builder import Criterion
+from frappe.utils import cint, cstr, formatdate, get_datetime, get_link_to_form, getdate, nowdate
 
+from erpnext.hr.doctype.shift_assignment.shift_assignment import has_overlapping_timings
 from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee
 
 
+class DuplicateAttendanceError(frappe.ValidationError):
+	pass
+
+
+class OverlappingShiftAttendanceError(frappe.ValidationError):
+	pass
+
+
 class Attendance(Document):
 	def validate(self):
 		from erpnext.controllers.status_updater import validate_status
@@ -18,6 +28,7 @@
 		validate_active_employee(self.employee)
 		self.validate_attendance_date()
 		self.validate_duplicate_record()
+		self.validate_overlapping_shift_attendance()
 		self.validate_employee_status()
 		self.check_leave_record()
 
@@ -35,21 +46,35 @@
 			frappe.throw(_("Attendance date can not be less than employee's joining date"))
 
 	def validate_duplicate_record(self):
-		res = frappe.db.sql(
-			"""
-			select name from `tabAttendance`
-			where employee = %s
-				and attendance_date = %s
-				and name != %s
-				and docstatus != 2
-		""",
-			(self.employee, getdate(self.attendance_date), self.name),
+		duplicate = get_duplicate_attendance_record(
+			self.employee, self.attendance_date, self.shift, self.name
 		)
-		if res:
+
+		if duplicate:
 			frappe.throw(
-				_("Attendance for employee {0} is already marked for the date {1}").format(
-					frappe.bold(self.employee), frappe.bold(self.attendance_date)
-				)
+				_("Attendance for employee {0} is already marked for the date {1}: {2}").format(
+					frappe.bold(self.employee),
+					frappe.bold(self.attendance_date),
+					get_link_to_form("Attendance", duplicate[0].name),
+				),
+				title=_("Duplicate Attendance"),
+				exc=DuplicateAttendanceError,
+			)
+
+	def validate_overlapping_shift_attendance(self):
+		attendance = get_overlapping_shift_attendance(
+			self.employee, self.attendance_date, self.shift, self.name
+		)
+
+		if attendance:
+			frappe.throw(
+				_("Attendance for employee {0} is already marked for an overlapping shift {1}: {2}").format(
+					frappe.bold(self.employee),
+					frappe.bold(attendance.shift),
+					get_link_to_form("Attendance", attendance.name),
+				),
+				title=_("Overlapping Shift Attendance"),
+				exc=OverlappingShiftAttendanceError,
 			)
 
 	def validate_employee_status(self):
@@ -103,6 +128,69 @@
 			frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
 
 
+def get_duplicate_attendance_record(employee, attendance_date, shift, name=None):
+	attendance = frappe.qb.DocType("Attendance")
+	query = (
+		frappe.qb.from_(attendance)
+		.select(attendance.name)
+		.where((attendance.employee == employee) & (attendance.docstatus < 2))
+	)
+
+	if shift:
+		query = query.where(
+			Criterion.any(
+				[
+					Criterion.all(
+						[
+							((attendance.shift.isnull()) | (attendance.shift == "")),
+							(attendance.attendance_date == attendance_date),
+						]
+					),
+					Criterion.all(
+						[
+							((attendance.shift.isnotnull()) | (attendance.shift != "")),
+							(attendance.attendance_date == attendance_date),
+							(attendance.shift == shift),
+						]
+					),
+				]
+			)
+		)
+	else:
+		query = query.where((attendance.attendance_date == attendance_date))
+
+	if name:
+		query = query.where(attendance.name != name)
+
+	return query.run(as_dict=True)
+
+
+def get_overlapping_shift_attendance(employee, attendance_date, shift, name=None):
+	if not shift:
+		return {}
+
+	attendance = frappe.qb.DocType("Attendance")
+	query = (
+		frappe.qb.from_(attendance)
+		.select(attendance.name, attendance.shift)
+		.where(
+			(attendance.employee == employee)
+			& (attendance.docstatus < 2)
+			& (attendance.attendance_date == attendance_date)
+			& (attendance.shift != shift)
+		)
+	)
+
+	if name:
+		query = query.where(attendance.name != name)
+
+	overlapping_attendance = query.run(as_dict=True)
+
+	if overlapping_attendance and has_overlapping_timings(shift, overlapping_attendance[0].shift):
+		return overlapping_attendance[0]
+	return {}
+
+
 @frappe.whitelist()
 def get_events(start, end, filters=None):
 	events = []
@@ -141,28 +229,39 @@
 
 
 def mark_attendance(
-	employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False
+	employee,
+	attendance_date,
+	status,
+	shift=None,
+	leave_type=None,
+	ignore_validate=False,
+	late_entry=False,
+	early_exit=False,
 ):
-	if not frappe.db.exists(
-		"Attendance",
-		{"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
-	):
-		company = frappe.db.get_value("Employee", employee, "company")
-		attendance = frappe.get_doc(
-			{
-				"doctype": "Attendance",
-				"employee": employee,
-				"attendance_date": attendance_date,
-				"status": status,
-				"company": company,
-				"shift": shift,
-				"leave_type": leave_type,
-			}
-		)
-		attendance.flags.ignore_validate = ignore_validate
-		attendance.insert()
-		attendance.submit()
-		return attendance.name
+	if get_duplicate_attendance_record(employee, attendance_date, shift):
+		return
+
+	if get_overlapping_shift_attendance(employee, attendance_date, shift):
+		return
+
+	company = frappe.db.get_value("Employee", employee, "company")
+	attendance = frappe.get_doc(
+		{
+			"doctype": "Attendance",
+			"employee": employee,
+			"attendance_date": attendance_date,
+			"status": status,
+			"company": company,
+			"shift": shift,
+			"leave_type": leave_type,
+			"late_entry": late_entry,
+			"early_exit": early_exit,
+		}
+	)
+	attendance.flags.ignore_validate = ignore_validate
+	attendance.insert()
+	attendance.submit()
+	return attendance.name
 
 
 @frappe.whitelist()
diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py
index 058bc93..762d0f7 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -6,6 +6,8 @@
 from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate
 
 from erpnext.hr.doctype.attendance.attendance import (
+	DuplicateAttendanceError,
+	OverlappingShiftAttendanceError,
 	get_month_map,
 	get_unmarked_days,
 	mark_attendance,
@@ -23,11 +25,112 @@
 		from_date = get_year_start(getdate())
 		to_date = get_year_ending(getdate())
 		self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
+		frappe.db.delete("Attendance")
+
+	def test_duplicate_attendance(self):
+		employee = make_employee("test_duplicate_attendance@example.com", company="_Test Company")
+		date = nowdate()
+
+		mark_attendance(employee, date, "Present")
+		attendance = frappe.get_doc(
+			{
+				"doctype": "Attendance",
+				"employee": employee,
+				"attendance_date": date,
+				"status": "Absent",
+				"company": "_Test Company",
+			}
+		)
+
+		self.assertRaises(DuplicateAttendanceError, attendance.insert)
+
+	def test_duplicate_attendance_with_shift(self):
+		from erpnext.hr.doctype.shift_type.test_shift_type import setup_shift_type
+
+		employee = make_employee("test_duplicate_attendance@example.com", company="_Test Company")
+		date = nowdate()
+
+		shift_1 = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="10:00:00")
+		mark_attendance(employee, date, "Present", shift=shift_1.name)
+
+		# attendance record with shift
+		attendance = frappe.get_doc(
+			{
+				"doctype": "Attendance",
+				"employee": employee,
+				"attendance_date": date,
+				"status": "Absent",
+				"company": "_Test Company",
+				"shift": shift_1.name,
+			}
+		)
+
+		self.assertRaises(DuplicateAttendanceError, attendance.insert)
+
+		# attendance record without any shift
+		attendance = frappe.get_doc(
+			{
+				"doctype": "Attendance",
+				"employee": employee,
+				"attendance_date": date,
+				"status": "Absent",
+				"company": "_Test Company",
+			}
+		)
+
+		self.assertRaises(DuplicateAttendanceError, attendance.insert)
+
+	def test_overlapping_shift_attendance_validation(self):
+		from erpnext.hr.doctype.shift_type.test_shift_type import setup_shift_type
+
+		employee = make_employee("test_overlap_attendance@example.com", company="_Test Company")
+		date = nowdate()
+
+		shift_1 = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="10:00:00")
+		shift_2 = setup_shift_type(shift_type="Shift 2", start_time="09:30:00", end_time="11:00:00")
+
+		mark_attendance(employee, date, "Present", shift=shift_1.name)
+
+		# attendance record with overlapping shift
+		attendance = frappe.get_doc(
+			{
+				"doctype": "Attendance",
+				"employee": employee,
+				"attendance_date": date,
+				"status": "Absent",
+				"company": "_Test Company",
+				"shift": shift_2.name,
+			}
+		)
+
+		self.assertRaises(OverlappingShiftAttendanceError, attendance.insert)
+
+	def test_allow_attendance_with_different_shifts(self):
+		# allows attendance with 2 different non-overlapping shifts
+		from erpnext.hr.doctype.shift_type.test_shift_type import setup_shift_type
+
+		employee = make_employee("test_duplicate_attendance@example.com", company="_Test Company")
+		date = nowdate()
+
+		shift_1 = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="10:00:00")
+		shift_2 = setup_shift_type(shift_type="Shift 2", start_time="11:00:00", end_time="12:00:00")
+
+		mark_attendance(employee, date, "Present", shift_1.name)
+		frappe.get_doc(
+			{
+				"doctype": "Attendance",
+				"employee": employee,
+				"attendance_date": date,
+				"status": "Absent",
+				"company": "_Test Company",
+				"shift": shift_2.name,
+			}
+		).insert()
 
 	def test_mark_absent(self):
 		employee = make_employee("test_mark_absent@example.com")
 		date = nowdate()
-		frappe.db.delete("Attendance", {"employee": employee, "attendance_date": date})
+
 		attendance = mark_attendance(employee, date, "Absent")
 		fetch_attendance = frappe.get_value(
 			"Attendance", {"employee": employee, "attendance_date": date, "status": "Absent"}
@@ -42,7 +145,6 @@
 		employee = make_employee(
 			"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
 		)
-		frappe.db.delete("Attendance", {"employee": employee})
 		frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
 
 		first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
@@ -67,8 +169,6 @@
 		employee = make_employee(
 			"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
 		)
-		frappe.db.delete("Attendance", {"employee": employee})
-
 		frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
 
 		first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
@@ -95,7 +195,6 @@
 		employee = make_employee(
 			"test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date
 		)
-		frappe.db.delete("Attendance", {"employee": employee})
 
 		frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
 
diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py
index d849900..87bdddd 100644
--- a/erpnext/hr/doctype/department_approver/department_approver.py
+++ b/erpnext/hr/doctype/department_approver/department_approver.py
@@ -87,7 +87,7 @@
 			field_name, frappe.bold(employee.employee_name)
 		)
 		if department_list:
-			error_msg += _(" or for Department: {0}").format(frappe.bold(employee_department))
+			error_msg += " " + _("or for Department: {0}").format(frappe.bold(employee_department))
 		frappe.throw(error_msg, title=_(field_name + " Missing"))
 
 	return set(tuple(approver) for approver in approvers)
diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
index 87f48b7..64eb019 100644
--- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py
+++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
@@ -7,6 +7,10 @@
 from frappe.model.document import Document
 from frappe.utils import cint, get_datetime
 
+from erpnext.hr.doctype.attendance.attendance import (
+	get_duplicate_attendance_record,
+	get_overlapping_shift_attendance,
+)
 from erpnext.hr.doctype.shift_assignment.shift_assignment import (
 	get_actual_start_end_datetime_of_shift,
 )
@@ -33,24 +37,24 @@
 		shift_actual_timings = get_actual_start_end_datetime_of_shift(
 			self.employee, get_datetime(self.time), True
 		)
-		if shift_actual_timings[0] and shift_actual_timings[1]:
+		if shift_actual_timings:
 			if (
-				shift_actual_timings[2].shift_type.determine_check_in_and_check_out
+				shift_actual_timings.shift_type.determine_check_in_and_check_out
 				== "Strictly based on Log Type in Employee Checkin"
 				and not self.log_type
 				and not self.skip_auto_attendance
 			):
 				frappe.throw(
 					_("Log Type is required for check-ins falling in the shift: {0}.").format(
-						shift_actual_timings[2].shift_type.name
+						shift_actual_timings.shift_type.name
 					)
 				)
 			if not self.attendance:
-				self.shift = shift_actual_timings[2].shift_type.name
-				self.shift_actual_start = shift_actual_timings[0]
-				self.shift_actual_end = shift_actual_timings[1]
-				self.shift_start = shift_actual_timings[2].start_datetime
-				self.shift_end = shift_actual_timings[2].end_datetime
+				self.shift = shift_actual_timings.shift_type.name
+				self.shift_actual_start = shift_actual_timings.actual_start
+				self.shift_actual_end = shift_actual_timings.actual_end
+				self.shift_start = shift_actual_timings.start_datetime
+				self.shift_end = shift_actual_timings.end_datetime
 		else:
 			self.shift = None
 
@@ -136,10 +140,10 @@
 		return None
 	elif attendance_status in ("Present", "Absent", "Half Day"):
 		employee_doc = frappe.get_doc("Employee", employee)
-		if not frappe.db.exists(
-			"Attendance",
-			{"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
-		):
+		duplicate = get_duplicate_attendance_record(employee, attendance_date, shift)
+		overlapping = get_overlapping_shift_attendance(employee, attendance_date, shift)
+
+		if not duplicate and not overlapping:
 			doc_dict = {
 				"doctype": "Attendance",
 				"employee": employee,
@@ -232,7 +236,7 @@
 
 
 def time_diff_in_hours(start, end):
-	return round((end - start).total_seconds() / 3600, 1)
+	return round(float((end - start).total_seconds()) / 3600, 2)
 
 
 def find_index_in_dict(dict_list, key, value):
diff --git a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
index 97f76b0..81b44f8 100644
--- a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
+++ b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
@@ -2,10 +2,19 @@
 # See license.txt
 
 import unittest
-from datetime import timedelta
+from datetime import datetime, timedelta
 
 import frappe
-from frappe.utils import now_datetime, nowdate
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import (
+	add_days,
+	get_time,
+	get_year_ending,
+	get_year_start,
+	getdate,
+	now_datetime,
+	nowdate,
+)
 
 from erpnext.hr.doctype.employee.test_employee import make_employee
 from erpnext.hr.doctype.employee_checkin.employee_checkin import (
@@ -13,9 +22,22 @@
 	calculate_working_hours,
 	mark_attendance_and_link_log,
 )
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
+from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
+from erpnext.hr.doctype.shift_type.test_shift_type import make_shift_assignment, setup_shift_type
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
 
 
-class TestEmployeeCheckin(unittest.TestCase):
+class TestEmployeeCheckin(FrappeTestCase):
+	def setUp(self):
+		frappe.db.delete("Shift Type")
+		frappe.db.delete("Shift Assignment")
+		frappe.db.delete("Employee Checkin")
+
+		from_date = get_year_start(getdate())
+		to_date = get_year_ending(getdate())
+		self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
+
 	def test_add_log_based_on_employee_field(self):
 		employee = make_employee("test_add_log_based_on_employee_field@example.com")
 		employee = frappe.get_doc("Employee", employee)
@@ -103,6 +125,163 @@
 		)
 		self.assertEqual(working_hours, (4.5, logs_type_2[1].time, logs_type_2[-1].time))
 
+	def test_fetch_shift(self):
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+
+		# shift setup for 8-12
+		shift_type = setup_shift_type()
+		date = getdate()
+		make_shift_assignment(shift_type.name, employee, date)
+
+		# within shift time
+		timestamp = datetime.combine(date, get_time("08:45:00"))
+		log = make_checkin(employee, timestamp)
+		self.assertEqual(log.shift, shift_type.name)
+
+		# "begin checkin before shift time" = 60 mins, so should work for 7:00:00
+		timestamp = datetime.combine(date, get_time("07:00:00"))
+		log = make_checkin(employee, timestamp)
+		self.assertEqual(log.shift, shift_type.name)
+
+		# "allow checkout after shift end time" = 60 mins, so should work for 13:00:00
+		timestamp = datetime.combine(date, get_time("13:00:00"))
+		log = make_checkin(employee, timestamp)
+		self.assertEqual(log.shift, shift_type.name)
+
+		# should not fetch this shift beyond allowed time
+		timestamp = datetime.combine(date, get_time("13:01:00"))
+		log = make_checkin(employee, timestamp)
+		self.assertIsNone(log.shift)
+
+	def test_shift_start_and_end_timings(self):
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+
+		# shift setup for 8-12
+		shift_type = setup_shift_type()
+		date = getdate()
+		make_shift_assignment(shift_type.name, employee, date)
+
+		timestamp = datetime.combine(date, get_time("08:45:00"))
+		log = make_checkin(employee, timestamp)
+
+		self.assertEqual(log.shift, shift_type.name)
+		self.assertEqual(log.shift_start, datetime.combine(date, get_time("08:00:00")))
+		self.assertEqual(log.shift_end, datetime.combine(date, get_time("12:00:00")))
+		self.assertEqual(log.shift_actual_start, datetime.combine(date, get_time("07:00:00")))
+		self.assertEqual(log.shift_actual_end, datetime.combine(date, get_time("13:00:00")))
+
+	def test_fetch_shift_based_on_default_shift(self):
+		employee = make_employee("test_default_shift@example.com", company="_Test Company")
+		default_shift = setup_shift_type(
+			shift_type="Default Shift", start_time="14:00:00", end_time="16:00:00"
+		)
+
+		date = getdate()
+		frappe.db.set_value("Employee", employee, "default_shift", default_shift.name)
+
+		timestamp = datetime.combine(date, get_time("14:45:00"))
+		log = make_checkin(employee, timestamp)
+
+		# should consider default shift
+		self.assertEqual(log.shift, default_shift.name)
+
+	def test_fetch_shift_spanning_over_two_days(self):
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+		shift_type = setup_shift_type(
+			shift_type="Midnight Shift", start_time="23:00:00", end_time="01:00:00"
+		)
+		date = getdate()
+		next_day = add_days(date, 1)
+		make_shift_assignment(shift_type.name, employee, date)
+
+		# log falls in the first day
+		timestamp = datetime.combine(date, get_time("23:00:00"))
+		log = make_checkin(employee, timestamp)
+
+		self.assertEqual(log.shift, shift_type.name)
+		self.assertEqual(log.shift_start, datetime.combine(date, get_time("23:00:00")))
+		self.assertEqual(log.shift_end, datetime.combine(next_day, get_time("01:00:00")))
+		self.assertEqual(log.shift_actual_start, datetime.combine(date, get_time("22:00:00")))
+		self.assertEqual(log.shift_actual_end, datetime.combine(next_day, get_time("02:00:00")))
+
+		log.delete()
+
+		# log falls in the second day
+		prev_day = add_days(date, -1)
+		timestamp = datetime.combine(date, get_time("01:30:00"))
+		log = make_checkin(employee, timestamp)
+		self.assertEqual(log.shift, shift_type.name)
+		self.assertEqual(log.shift_start, datetime.combine(prev_day, get_time("23:00:00")))
+		self.assertEqual(log.shift_end, datetime.combine(date, get_time("01:00:00")))
+		self.assertEqual(log.shift_actual_start, datetime.combine(prev_day, get_time("22:00:00")))
+		self.assertEqual(log.shift_actual_end, datetime.combine(date, get_time("02:00:00")))
+
+	def test_no_shift_fetched_on_holiday_as_per_shift_holiday_list(self):
+		date = getdate()
+		from_date = get_year_start(date)
+		to_date = get_year_ending(date)
+		holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
+
+		employee = make_employee("test_shift_with_holiday@example.com", company="_Test Company")
+		setup_shift_type(shift_type="Test Holiday Shift", holiday_list=holiday_list)
+
+		first_sunday = get_first_sunday(holiday_list, for_date=date)
+		timestamp = datetime.combine(first_sunday, get_time("08:00:00"))
+		log = make_checkin(employee, timestamp)
+
+		self.assertIsNone(log.shift)
+
+	@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+	def test_no_shift_fetched_on_holiday_as_per_employee_holiday_list(self):
+		employee = make_employee("test_shift_with_holiday@example.com", company="_Test Company")
+		shift_type = setup_shift_type(shift_type="Test Holiday Shift")
+		shift_type.holiday_list = None
+		shift_type.save()
+
+		date = getdate()
+
+		first_sunday = get_first_sunday(self.holiday_list, for_date=date)
+		timestamp = datetime.combine(first_sunday, get_time("08:00:00"))
+		log = make_checkin(employee, timestamp)
+
+		self.assertIsNone(log.shift)
+
+	def test_consecutive_shift_assignments_overlapping_within_grace_period(self):
+		# test adjustment for start and end times if they are overlapping
+		# within "begin_check_in_before_shift_start_time" and "allow_check_out_after_shift_end_time" periods
+		employee = make_employee("test_shift@example.com", company="_Test Company")
+
+		# 8 - 12
+		shift1 = setup_shift_type()
+		# 12:30 - 16:30
+		shift2 = setup_shift_type(
+			shift_type="Consecutive Shift", start_time="12:30:00", end_time="16:30:00"
+		)
+
+		# the actual start and end times (with grace) for these shifts are 7 - 13 and 11:30 - 17:30
+		date = getdate()
+		make_shift_assignment(shift1.name, employee, date)
+		make_shift_assignment(shift2.name, employee, date)
+
+		# log at 12:30 should set shift2 and actual start as 12 and not 11:30
+		timestamp = datetime.combine(date, get_time("12:30:00"))
+		log = make_checkin(employee, timestamp)
+		self.assertEqual(log.shift, shift2.name)
+		self.assertEqual(log.shift_start, datetime.combine(date, get_time("12:30:00")))
+		self.assertEqual(log.shift_actual_start, datetime.combine(date, get_time("12:00:00")))
+
+		# log at 12:00 should set shift1 and actual end as 12 and not 1 since the next shift's grace starts
+		timestamp = datetime.combine(date, get_time("12:00:00"))
+		log = make_checkin(employee, timestamp)
+		self.assertEqual(log.shift, shift1.name)
+		self.assertEqual(log.shift_end, datetime.combine(date, get_time("12:00:00")))
+		self.assertEqual(log.shift_actual_end, datetime.combine(date, get_time("12:00:00")))
+
+		# log at 12:01 should set shift2
+		timestamp = datetime.combine(date, get_time("12:01:00"))
+		log = make_checkin(employee, timestamp)
+		self.assertEqual(log.shift, shift2.name)
+
 
 def make_n_checkins(employee, n, hours_to_reverse=1):
 	logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n + 1))]
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
index 9742387..aef4412 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
@@ -34,6 +34,15 @@
 				});
 			}
 		}
+
+		// make new leaves allocated field read only if allocation is created via leave policy assignment
+		// and leave type is earned leave, since these leaves would be allocated via the scheduler
+		if (frm.doc.leave_policy_assignment) {
+			frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => {
+				if (r && cint(r.is_earned_leave))
+					frm.set_df_property("new_leaves_allocated", "read_only", 1);
+			});
+		}
 	},
 
 	expire_allocation: function(frm) {
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index 9ecbe01..9d1db9b 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -237,7 +237,7 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-01-18 19:15:53.262536",
+ "modified": "2022-04-07 09:50:33.145825",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "Leave Allocation",
@@ -281,5 +281,6 @@
  "sort_order": "DESC",
  "states": [],
  "timeline_field": "employee",
- "title_field": "employee_name"
+ "title_field": "employee_name",
+ "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index 98408af..27479a5 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -39,11 +39,15 @@
 	def validate(self):
 		self.validate_period()
 		self.validate_allocation_overlap()
-		self.validate_back_dated_allocation()
-		self.set_total_leaves_allocated()
-		self.validate_total_leaves_allocated()
 		self.validate_lwp()
 		set_employee_name(self)
+		self.set_total_leaves_allocated()
+		self.validate_leave_days_and_dates()
+
+	def validate_leave_days_and_dates(self):
+		# all validations that should run on save as well as on update after submit
+		self.validate_back_dated_allocation()
+		self.validate_total_leaves_allocated()
 		self.validate_leave_allocation_days()
 
 	def validate_leave_allocation_days(self):
@@ -56,14 +60,19 @@
 			leave_allocated = 0
 			if leave_period:
 				leave_allocated = get_leave_allocation_for_period(
-					self.employee, self.leave_type, leave_period[0].from_date, leave_period[0].to_date
+					self.employee,
+					self.leave_type,
+					leave_period[0].from_date,
+					leave_period[0].to_date,
+					exclude_allocation=self.name,
 				)
 			leave_allocated += flt(self.new_leaves_allocated)
 			if leave_allocated > max_leaves_allowed:
 				frappe.throw(
 					_(
-						"Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period"
-					).format(self.leave_type, self.employee)
+						"Total allocated leaves are more than maximum allocation allowed for {0} leave type for employee {1} in the period"
+					).format(self.leave_type, self.employee),
+					OverAllocationError,
 				)
 
 	def on_submit(self):
@@ -84,6 +93,12 @@
 	def on_update_after_submit(self):
 		if self.has_value_changed("new_leaves_allocated"):
 			self.validate_against_leave_applications()
+
+			# recalculate total leaves allocated
+			self.total_leaves_allocated = flt(self.unused_leaves) + flt(self.new_leaves_allocated)
+			# run required validations again since total leaves are being updated
+			self.validate_leave_days_and_dates()
+
 			leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
 			args = {
 				"leaves": leaves_to_be_added,
@@ -92,6 +107,7 @@
 				"is_carry_forward": 0,
 			}
 			create_leave_ledger_entry(self, args, True)
+			self.db_update()
 
 	def get_existing_leave_count(self):
 		ledger_entries = frappe.get_all(
@@ -279,27 +295,27 @@
 	)
 
 
-def get_leave_allocation_for_period(employee, leave_type, from_date, to_date):
-	leave_allocated = 0
-	leave_allocations = frappe.db.sql(
-		"""
-		select employee, leave_type, from_date, to_date, total_leaves_allocated
-		from `tabLeave Allocation`
-		where employee=%(employee)s and leave_type=%(leave_type)s
-			and docstatus=1
-			and (from_date between %(from_date)s and %(to_date)s
-				or to_date between %(from_date)s and %(to_date)s
-				or (from_date < %(from_date)s and to_date > %(to_date)s))
-	""",
-		{"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type},
-		as_dict=1,
-	)
+def get_leave_allocation_for_period(
+	employee, leave_type, from_date, to_date, exclude_allocation=None
+):
+	from frappe.query_builder.functions import Sum
 
-	if leave_allocations:
-		for leave_alloc in leave_allocations:
-			leave_allocated += leave_alloc.total_leaves_allocated
-
-	return leave_allocated
+	Allocation = frappe.qb.DocType("Leave Allocation")
+	return (
+		frappe.qb.from_(Allocation)
+		.select(Sum(Allocation.total_leaves_allocated).as_("total_allocated_leaves"))
+		.where(
+			(Allocation.employee == employee)
+			& (Allocation.leave_type == leave_type)
+			& (Allocation.docstatus == 1)
+			& (Allocation.name != exclude_allocation)
+			& (
+				(Allocation.from_date.between(from_date, to_date))
+				| (Allocation.to_date.between(from_date, to_date))
+				| ((Allocation.from_date < from_date) & (Allocation.to_date > to_date))
+			)
+		)
+	).run()[0][0] or 0.0
 
 
 @frappe.whitelist()
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
index a53d4a8..dde52d7 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
@@ -1,24 +1,26 @@
 import unittest
 
 import frappe
+from frappe.tests.utils import FrappeTestCase
 from frappe.utils import add_days, add_months, getdate, nowdate
 
 import erpnext
 from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.leave_allocation.leave_allocation import (
+	BackDatedAllocationError,
+	OverAllocationError,
+)
 from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
 from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
 
 
-class TestLeaveAllocation(unittest.TestCase):
-	@classmethod
-	def setUpClass(cls):
-		frappe.db.sql("delete from `tabLeave Period`")
+class TestLeaveAllocation(FrappeTestCase):
+	def setUp(self):
+		frappe.db.delete("Leave Period")
+		frappe.db.delete("Leave Allocation")
 
-		emp_id = make_employee("test_emp_leave_allocation@salary.com")
-		cls.employee = frappe.get_doc("Employee", emp_id)
-
-	def tearDown(self):
-		frappe.db.rollback()
+		emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company")
+		self.employee = frappe.get_doc("Employee", emp_id)
 
 	def test_overlapping_allocation(self):
 		leaves = [
@@ -65,7 +67,7 @@
 		# invalid period
 		self.assertRaises(frappe.ValidationError, doc.save)
 
-	def test_allocated_leave_days_over_period(self):
+	def test_validation_for_over_allocation(self):
 		doc = frappe.get_doc(
 			{
 				"doctype": "Leave Allocation",
@@ -80,7 +82,135 @@
 		)
 
 		# allocated leave more than period
-		self.assertRaises(frappe.ValidationError, doc.save)
+		self.assertRaises(OverAllocationError, doc.save)
+
+	def test_validation_for_over_allocation_post_submission(self):
+		allocation = frappe.get_doc(
+			{
+				"doctype": "Leave Allocation",
+				"__islocal": 1,
+				"employee": self.employee.name,
+				"employee_name": self.employee.employee_name,
+				"leave_type": "_Test Leave Type",
+				"from_date": getdate("2015-09-1"),
+				"to_date": getdate("2015-09-30"),
+				"new_leaves_allocated": 15,
+			}
+		).submit()
+		allocation.reload()
+		# allocated leaves more than period after submission
+		allocation.new_leaves_allocated = 35
+		self.assertRaises(OverAllocationError, allocation.save)
+
+	def test_validation_for_over_allocation_based_on_leave_setup(self):
+		frappe.delete_doc_if_exists("Leave Period", "Test Allocation Period")
+		leave_period = frappe.get_doc(
+			dict(
+				name="Test Allocation Period",
+				doctype="Leave Period",
+				from_date=add_months(nowdate(), -6),
+				to_date=add_months(nowdate(), 6),
+				company="_Test Company",
+				is_active=1,
+			)
+		).insert()
+
+		leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
+		leave_type.max_leaves_allowed = 25
+		leave_type.save()
+
+		# 15 leaves allocated in this period
+		allocation = create_leave_allocation(
+			leave_type=leave_type.name,
+			employee=self.employee.name,
+			employee_name=self.employee.employee_name,
+			from_date=leave_period.from_date,
+			to_date=nowdate(),
+		)
+		allocation.submit()
+
+		# trying to allocate additional 15 leaves
+		allocation = create_leave_allocation(
+			leave_type=leave_type.name,
+			employee=self.employee.name,
+			employee_name=self.employee.employee_name,
+			from_date=add_days(nowdate(), 1),
+			to_date=leave_period.to_date,
+		)
+		self.assertRaises(OverAllocationError, allocation.save)
+
+	def test_validation_for_over_allocation_based_on_leave_setup_post_submission(self):
+		frappe.delete_doc_if_exists("Leave Period", "Test Allocation Period")
+		leave_period = frappe.get_doc(
+			dict(
+				name="Test Allocation Period",
+				doctype="Leave Period",
+				from_date=add_months(nowdate(), -6),
+				to_date=add_months(nowdate(), 6),
+				company="_Test Company",
+				is_active=1,
+			)
+		).insert()
+
+		leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
+		leave_type.max_leaves_allowed = 30
+		leave_type.save()
+
+		# 15 leaves allocated
+		allocation = create_leave_allocation(
+			leave_type=leave_type.name,
+			employee=self.employee.name,
+			employee_name=self.employee.employee_name,
+			from_date=leave_period.from_date,
+			to_date=nowdate(),
+		)
+		allocation.submit()
+		allocation.reload()
+
+		# allocate additional 15 leaves
+		allocation = create_leave_allocation(
+			leave_type=leave_type.name,
+			employee=self.employee.name,
+			employee_name=self.employee.employee_name,
+			from_date=add_days(nowdate(), 1),
+			to_date=leave_period.to_date,
+		)
+		allocation.submit()
+		allocation.reload()
+
+		# trying to allocate 25 leaves in 2nd alloc within leave period
+		# total leaves = 40 which is more than `max_leaves_allowed` setting i.e. 30
+		allocation.new_leaves_allocated = 25
+		self.assertRaises(OverAllocationError, allocation.save)
+
+	def test_validate_back_dated_allocation_update(self):
+		leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
+		leave_type.save()
+
+		# initial leave allocation = 15
+		leave_allocation = create_leave_allocation(
+			employee=self.employee.name,
+			employee_name=self.employee.employee_name,
+			leave_type="_Test_CF_leave",
+			from_date=add_months(nowdate(), -12),
+			to_date=add_months(nowdate(), -1),
+			carry_forward=0,
+		)
+		leave_allocation.submit()
+
+		# new_leaves = 15, carry_forwarded = 10
+		leave_allocation_1 = create_leave_allocation(
+			employee=self.employee.name,
+			employee_name=self.employee.employee_name,
+			leave_type="_Test_CF_leave",
+			carry_forward=1,
+		)
+		leave_allocation_1.submit()
+
+		# try updating initial leave allocation
+		leave_allocation.reload()
+		leave_allocation.new_leaves_allocated = 20
+		self.assertRaises(BackDatedAllocationError, leave_allocation.save)
 
 	def test_carry_forward_calculation(self):
 		leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
@@ -108,8 +238,10 @@
 			carry_forward=1,
 		)
 		leave_allocation_1.submit()
+		leave_allocation_1.reload()
 
 		self.assertEqual(leave_allocation_1.unused_leaves, 10)
+		self.assertEqual(leave_allocation_1.total_leaves_allocated, 25)
 
 		leave_allocation_1.cancel()
 
@@ -197,9 +329,12 @@
 			employee=self.employee.name, employee_name=self.employee.employee_name
 		)
 		leave_allocation.submit()
+		leave_allocation.reload()
 		self.assertTrue(leave_allocation.total_leaves_allocated, 15)
+
 		leave_allocation.new_leaves_allocated = 40
 		leave_allocation.submit()
+		leave_allocation.reload()
 		self.assertTrue(leave_allocation.total_leaves_allocated, 40)
 
 	def test_leave_subtraction_after_submit(self):
@@ -207,9 +342,12 @@
 			employee=self.employee.name, employee_name=self.employee.employee_name
 		)
 		leave_allocation.submit()
+		leave_allocation.reload()
 		self.assertTrue(leave_allocation.total_leaves_allocated, 15)
+
 		leave_allocation.new_leaves_allocated = 10
 		leave_allocation.submit()
+		leave_allocation.reload()
 		self.assertTrue(leave_allocation.total_leaves_allocated, 10)
 
 	def test_validation_against_leave_application_after_submit(self):
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 18c69f7..cd6b168 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -735,9 +735,9 @@
 	(Based on the include_holiday setting in Leave Type)"""
 	number_of_days = 0
 	if cint(half_day) == 1:
-		if from_date == to_date:
+		if getdate(from_date) == getdate(to_date):
 			number_of_days = 0.5
-		elif half_day_date and half_day_date <= to_date:
+		elif half_day_date and getdate(from_date) <= getdate(half_day_date) <= getdate(to_date):
 			number_of_days = date_diff(to_date, from_date) + 0.5
 		else:
 			number_of_days = date_diff(to_date, from_date) + 1
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index f33d0af..4c39e15 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -205,7 +205,12 @@
 		# creates separate leave ledger entries
 		frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1)
 		leave_type = frappe.get_doc(
-			dict(leave_type_name="Test Leave Validation", doctype="Leave Type", allow_negative=True)
+			dict(
+				leave_type_name="Test Leave Validation",
+				doctype="Leave Type",
+				allow_negative=True,
+				include_holiday=True,
+			)
 		).insert()
 
 		employee = get_employee()
@@ -217,8 +222,14 @@
 		# application across allocations
 
 		# CASE 1: from date has no allocation, to date has an allocation / both dates have allocation
+		start_date = add_days(year_start, -10)
 		application = make_leave_application(
-			employee.name, add_days(year_start, -10), add_days(year_start, 3), leave_type.name
+			employee.name,
+			start_date,
+			add_days(year_start, 3),
+			leave_type.name,
+			half_day=1,
+			half_day_date=start_date,
 		)
 
 		# 2 separate leave ledger entries
@@ -828,6 +839,7 @@
 			leave_type_name="_Test_CF_leave_expiry",
 			is_carry_forward=1,
 			expire_carry_forwarded_leaves_after_days=90,
+			include_holiday=True,
 		)
 		leave_type.submit()
 
@@ -840,6 +852,8 @@
 				leave_type=leave_type.name,
 				from_date=add_days(nowdate(), -3),
 				to_date=add_days(nowdate(), 7),
+				half_day=1,
+				half_day_date=add_days(nowdate(), -3),
 				description="_Test Reason",
 				company="_Test Company",
 				docstatus=1,
@@ -855,7 +869,7 @@
 		self.assertEqual(len(leave_ledger_entry), 2)
 		self.assertEqual(leave_ledger_entry[0].employee, leave_application.employee)
 		self.assertEqual(leave_ledger_entry[0].leave_type, leave_application.leave_type)
-		self.assertEqual(leave_ledger_entry[0].leaves, -9)
+		self.assertEqual(leave_ledger_entry[0].leaves, -8.5)
 		self.assertEqual(leave_ledger_entry[1].leaves, -2)
 
 	def test_leave_application_creation_after_expiry(self):
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
index 5a12486..0b21c00 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
@@ -3,83 +3,120 @@
 
 
 from datetime import datetime, timedelta
+from typing import Dict, List
 
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe.utils import cstr, getdate, now_datetime, nowdate
+from frappe.query_builder import Criterion
+from frappe.utils import cstr, get_datetime, get_link_to_form, get_time, getdate, now_datetime
 
 from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
 from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
 from erpnext.hr.utils import validate_active_employee
 
 
+class OverlappingShiftError(frappe.ValidationError):
+	pass
+
+
 class ShiftAssignment(Document):
 	def validate(self):
 		validate_active_employee(self.employee)
-		self.validate_overlapping_dates()
+		self.validate_overlapping_shifts()
 
 		if self.end_date:
 			self.validate_from_to_dates("start_date", "end_date")
 
-	def validate_overlapping_dates(self):
+	def validate_overlapping_shifts(self):
+		overlapping_dates = self.get_overlapping_dates()
+		if len(overlapping_dates):
+			# if dates are overlapping, check if timings are overlapping, else allow
+			overlapping_timings = has_overlapping_timings(self.shift_type, overlapping_dates[0].shift_type)
+			if overlapping_timings:
+				self.throw_overlap_error(overlapping_dates[0])
+
+	def get_overlapping_dates(self):
 		if not self.name:
 			self.name = "New Shift Assignment"
 
-		condition = """and (
-				end_date is null
-				or
-					%(start_date)s between start_date and end_date
-		"""
-
-		if self.end_date:
-			condition += """ or
-					%(end_date)s between start_date and end_date
-					or
-					start_date between %(start_date)s and %(end_date)s
-				) """
-		else:
-			condition += """ ) """
-
-		assigned_shifts = frappe.db.sql(
-			"""
-			select name, shift_type, start_date ,end_date, docstatus, status
-			from `tabShift Assignment`
-			where
-				employee=%(employee)s and docstatus = 1
-				and name != %(name)s
-				and status = "Active"
-				{0}
-		""".format(
-				condition
-			),
-			{
-				"employee": self.employee,
-				"shift_type": self.shift_type,
-				"start_date": self.start_date,
-				"end_date": self.end_date,
-				"name": self.name,
-			},
-			as_dict=1,
+		shift = frappe.qb.DocType("Shift Assignment")
+		query = (
+			frappe.qb.from_(shift)
+			.select(shift.name, shift.shift_type, shift.docstatus, shift.status)
+			.where(
+				(shift.employee == self.employee)
+				& (shift.docstatus == 1)
+				& (shift.name != self.name)
+				& (shift.status == "Active")
+			)
 		)
 
-		if len(assigned_shifts):
-			self.throw_overlap_error(assigned_shifts[0])
+		if self.end_date:
+			query = query.where(
+				Criterion.any(
+					[
+						Criterion.any(
+							[
+								shift.end_date.isnull(),
+								((self.start_date >= shift.start_date) & (self.start_date <= shift.end_date)),
+							]
+						),
+						Criterion.any(
+							[
+								((self.end_date >= shift.start_date) & (self.end_date <= shift.end_date)),
+								shift.start_date.between(self.start_date, self.end_date),
+							]
+						),
+					]
+				)
+			)
+		else:
+			query = query.where(
+				shift.end_date.isnull()
+				| ((self.start_date >= shift.start_date) & (self.start_date <= shift.end_date))
+			)
+
+		return query.run(as_dict=True)
 
 	def throw_overlap_error(self, shift_details):
 		shift_details = frappe._dict(shift_details)
 		if shift_details.docstatus == 1 and shift_details.status == "Active":
-			msg = _("Employee {0} already has Active Shift {1}: {2}").format(
-				frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name)
+			msg = _(
+				"Employee {0} already has an active Shift {1}: {2} that overlaps within this period."
+			).format(
+				frappe.bold(self.employee),
+				frappe.bold(shift_details.shift_type),
+				get_link_to_form("Shift Assignment", shift_details.name),
 			)
-		if shift_details.start_date:
-			msg += _(" from {0}").format(getdate(self.start_date).strftime("%d-%m-%Y"))
-			title = "Ongoing Shift"
-			if shift_details.end_date:
-				msg += _(" to {0}").format(getdate(self.end_date).strftime("%d-%m-%Y"))
-				title = "Active Shift"
-		if msg:
-			frappe.throw(msg, title=title)
+			frappe.throw(msg, title=_("Overlapping Shifts"), exc=OverlappingShiftError)
+
+
+def has_overlapping_timings(shift_1: str, shift_2: str) -> bool:
+	"""
+	Accepts two shift types and checks whether their timings are overlapping
+	"""
+	curr_shift = frappe.db.get_value("Shift Type", shift_1, ["start_time", "end_time"], as_dict=True)
+	overlapping_shift = frappe.db.get_value(
+		"Shift Type", shift_2, ["start_time", "end_time"], as_dict=True
+	)
+
+	if (
+		(
+			curr_shift.start_time > overlapping_shift.start_time
+			and curr_shift.start_time < overlapping_shift.end_time
+		)
+		or (
+			curr_shift.end_time > overlapping_shift.start_time
+			and curr_shift.end_time < overlapping_shift.end_time
+		)
+		or (
+			curr_shift.start_time <= overlapping_shift.start_time
+			and curr_shift.end_time >= overlapping_shift.end_time
+		)
+	):
+		return True
+	return False
 
 
 @frappe.whitelist()
@@ -155,102 +192,195 @@
 	return shift_timing_map
 
 
+def get_shift_for_time(shifts: List[Dict], for_timestamp: datetime) -> Dict:
+	"""Returns shift with details for given timestamp"""
+	valid_shifts = []
+
+	for entry in shifts:
+		shift_details = get_shift_details(entry.shift_type, for_timestamp=for_timestamp)
+
+		if (
+			get_datetime(shift_details.actual_start)
+			<= get_datetime(for_timestamp)
+			<= get_datetime(shift_details.actual_end)
+		):
+			valid_shifts.append(shift_details)
+
+	valid_shifts.sort(key=lambda x: x["actual_start"])
+
+	if len(valid_shifts) > 1:
+		for i in range(len(valid_shifts) - 1):
+			# comparing 2 consecutive shifts and adjusting start and end times
+			# if they are overlapping within grace period
+			curr_shift = valid_shifts[i]
+			next_shift = valid_shifts[i + 1]
+
+			if curr_shift and next_shift:
+				next_shift.actual_start = (
+					curr_shift.end_datetime
+					if next_shift.actual_start < curr_shift.end_datetime
+					else next_shift.actual_start
+				)
+				curr_shift.actual_end = (
+					next_shift.actual_start
+					if curr_shift.actual_end > next_shift.actual_start
+					else curr_shift.actual_end
+				)
+
+			valid_shifts[i] = curr_shift
+			valid_shifts[i + 1] = next_shift
+
+		return get_exact_shift(valid_shifts, for_timestamp) or {}
+
+	return (valid_shifts and valid_shifts[0]) or {}
+
+
+def get_shifts_for_date(employee: str, for_timestamp: datetime) -> List[Dict[str, str]]:
+	"""Returns list of shifts with details for given date"""
+	assignment = frappe.qb.DocType("Shift Assignment")
+
+	return (
+		frappe.qb.from_(assignment)
+		.select(assignment.name, assignment.shift_type)
+		.where(
+			(assignment.employee == employee)
+			& (assignment.docstatus == 1)
+			& (assignment.status == "Active")
+			& (assignment.start_date <= getdate(for_timestamp.date()))
+			& (
+				Criterion.any(
+					[
+						assignment.end_date.isnull(),
+						(assignment.end_date.isnotnull() & (getdate(for_timestamp.date()) >= assignment.end_date)),
+					]
+				)
+			)
+		)
+	).run(as_dict=True)
+
+
+def get_shift_for_timestamp(employee: str, for_timestamp: datetime) -> Dict:
+	shifts = get_shifts_for_date(employee, for_timestamp)
+	if shifts:
+		return get_shift_for_time(shifts, for_timestamp)
+	return {}
+
+
 def get_employee_shift(
-	employee, for_date=None, consider_default_shift=False, next_shift_direction=None
-):
+	employee: str,
+	for_timestamp: datetime = None,
+	consider_default_shift: bool = False,
+	next_shift_direction: str = None,
+) -> Dict:
 	"""Returns a Shift Type for the given employee on the given date. (excluding the holidays)
 
 	:param employee: Employee for which shift is required.
-	:param for_date: Date on which shift are required
+	:param for_timestamp: DateTime on which shift is required
 	:param consider_default_shift: If set to true, default shift is taken when no shift assignment is found.
 	:param next_shift_direction: One of: None, 'forward', 'reverse'. Direction to look for next shift if shift not found on given date.
 	"""
-	if for_date is None:
-		for_date = nowdate()
+	if for_timestamp is None:
+		for_timestamp = now_datetime()
+
+	shift_details = get_shift_for_timestamp(employee, for_timestamp)
+
+	# if shift assignment is not found, consider default shift
 	default_shift = frappe.db.get_value("Employee", employee, "default_shift")
-	shift_type_name = None
-	shift_assignment_details = frappe.db.get_value(
-		"Shift Assignment",
-		{"employee": employee, "start_date": ("<=", for_date), "docstatus": "1", "status": "Active"},
-		["shift_type", "end_date"],
+	if not shift_details and consider_default_shift:
+		shift_details = get_shift_details(default_shift, for_timestamp)
+
+	# if its a holiday, reset
+	if shift_details and is_holiday_date(employee, shift_details):
+		shift_details = None
+
+	# if no shift is found, find next or prev shift assignment based on direction
+	if not shift_details and next_shift_direction:
+		shift_details = get_prev_or_next_shift(
+			employee, for_timestamp, consider_default_shift, default_shift, next_shift_direction
+		)
+
+	return shift_details or {}
+
+
+def get_prev_or_next_shift(
+	employee: str,
+	for_timestamp: datetime,
+	consider_default_shift: bool,
+	default_shift: str,
+	next_shift_direction: str,
+) -> Dict:
+	"""Returns a dict of shift details for the next or prev shift based on the next_shift_direction"""
+	MAX_DAYS = 366
+	shift_details = {}
+
+	if consider_default_shift and default_shift:
+		direction = -1 if next_shift_direction == "reverse" else 1
+		for i in range(MAX_DAYS):
+			date = for_timestamp + timedelta(days=direction * (i + 1))
+			shift_details = get_employee_shift(employee, date, consider_default_shift, None)
+			if shift_details:
+				break
+	else:
+		direction = "<" if next_shift_direction == "reverse" else ">"
+		sort_order = "desc" if next_shift_direction == "reverse" else "asc"
+		dates = frappe.db.get_all(
+			"Shift Assignment",
+			["start_date", "end_date"],
+			{
+				"employee": employee,
+				"start_date": (direction, for_timestamp.date()),
+				"docstatus": 1,
+				"status": "Active",
+			},
+			as_list=True,
+			limit=MAX_DAYS,
+			order_by="start_date " + sort_order,
+		)
+
+		if dates:
+			for date in dates:
+				if date[1] and date[1] < for_timestamp.date():
+					continue
+				shift_details = get_employee_shift(
+					employee, datetime.combine(date[0], for_timestamp.time()), consider_default_shift, None
+				)
+				if shift_details:
+					break
+
+	return shift_details or {}
+
+
+def is_holiday_date(employee: str, shift_details: Dict) -> bool:
+	holiday_list_name = frappe.db.get_value(
+		"Shift Type", shift_details.shift_type.name, "holiday_list"
 	)
 
-	if shift_assignment_details:
-		shift_type_name = shift_assignment_details[0]
+	if not holiday_list_name:
+		holiday_list_name = get_holiday_list_for_employee(employee, False)
 
-		# if end_date present means that shift is over after end_date else it is a ongoing shift.
-		if shift_assignment_details[1] and for_date >= shift_assignment_details[1]:
-			shift_type_name = None
-
-	if not shift_type_name and consider_default_shift:
-		shift_type_name = default_shift
-	if shift_type_name:
-		holiday_list_name = frappe.db.get_value("Shift Type", shift_type_name, "holiday_list")
-		if not holiday_list_name:
-			holiday_list_name = get_holiday_list_for_employee(employee, False)
-		if holiday_list_name and is_holiday(holiday_list_name, for_date):
-			shift_type_name = None
-
-	if not shift_type_name and next_shift_direction:
-		MAX_DAYS = 366
-		if consider_default_shift and default_shift:
-			direction = -1 if next_shift_direction == "reverse" else +1
-			for i in range(MAX_DAYS):
-				date = for_date + timedelta(days=direction * (i + 1))
-				shift_details = get_employee_shift(employee, date, consider_default_shift, None)
-				if shift_details:
-					shift_type_name = shift_details.shift_type.name
-					for_date = date
-					break
-		else:
-			direction = "<" if next_shift_direction == "reverse" else ">"
-			sort_order = "desc" if next_shift_direction == "reverse" else "asc"
-			dates = frappe.db.get_all(
-				"Shift Assignment",
-				["start_date", "end_date"],
-				{
-					"employee": employee,
-					"start_date": (direction, for_date),
-					"docstatus": "1",
-					"status": "Active",
-				},
-				as_list=True,
-				limit=MAX_DAYS,
-				order_by="start_date " + sort_order,
-			)
-
-			if dates:
-				for date in dates:
-					if date[1] and date[1] < for_date:
-						continue
-					shift_details = get_employee_shift(employee, date[0], consider_default_shift, None)
-					if shift_details:
-						shift_type_name = shift_details.shift_type.name
-						for_date = date[0]
-						break
-
-	return get_shift_details(shift_type_name, for_date)
+	return holiday_list_name and is_holiday(holiday_list_name, shift_details.start_datetime.date())
 
 
-def get_employee_shift_timings(employee, for_timestamp=None, consider_default_shift=False):
+def get_employee_shift_timings(
+	employee: str, for_timestamp: datetime = None, consider_default_shift: bool = False
+) -> List[Dict]:
 	"""Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee"""
 	if for_timestamp is None:
 		for_timestamp = now_datetime()
+
 	# write and verify a test case for midnight shift.
 	prev_shift = curr_shift = next_shift = None
-	curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, "forward")
+	curr_shift = get_employee_shift(employee, for_timestamp, consider_default_shift, "forward")
 	if curr_shift:
 		next_shift = get_employee_shift(
-			employee,
-			curr_shift.start_datetime.date() + timedelta(days=1),
-			consider_default_shift,
-			"forward",
+			employee, curr_shift.start_datetime + timedelta(days=1), consider_default_shift, "forward"
 		)
 	prev_shift = get_employee_shift(
-		employee, for_timestamp.date() + timedelta(days=-1), consider_default_shift, "reverse"
+		employee, for_timestamp + timedelta(days=-1), consider_default_shift, "reverse"
 	)
 
 	if curr_shift:
+		# adjust actual start and end times if they are overlapping with grace period (before start and after end)
 		if prev_shift:
 			curr_shift.actual_start = (
 				prev_shift.end_datetime
@@ -273,31 +403,102 @@
 				if curr_shift.actual_end > next_shift.actual_start
 				else curr_shift.actual_end
 			)
+
 	return prev_shift, curr_shift, next_shift
 
 
-def get_shift_details(shift_type_name, for_date=None):
-	"""Returns Shift Details which contain some additional information as described below.
-	'shift_details' contains the following keys:
-	        'shift_type' - Object of DocType Shift Type,
-	        'start_datetime' - Date and Time of shift start on given date,
-	        'end_datetime' - Date and Time of shift end on given date,
-	        'actual_start' - datetime of shift start after adding 'begin_check_in_before_shift_start_time',
-	        'actual_end' - datetime of shift end after adding 'allow_check_out_after_shift_end_time'(None is returned if this is zero)
+def get_actual_start_end_datetime_of_shift(
+	employee: str, for_timestamp: datetime, consider_default_shift: bool = False
+) -> Dict:
+	"""Returns a Dict containing shift details with actual_start and actual_end datetime values
+	Here 'actual' means taking into account the "begin_check_in_before_shift_start_time" and "allow_check_out_after_shift_end_time".
+	Empty Dict is returned if the timestamp is outside any actual shift timings.
 
-	:param shift_type_name: shift type name for which shift_details is required.
-	:param for_date: Date on which shift_details are required
+	:param employee (str): Employee name
+	:param for_timestamp (datetime, optional): Datetime value of checkin, if not provided considers current datetime
+	:param consider_default_shift (bool, optional): Flag (defaults to False) to specify whether to consider
+	default shift in employee master if no shift assignment is found
+	"""
+	shift_timings_as_per_timestamp = get_employee_shift_timings(
+		employee, for_timestamp, consider_default_shift
+	)
+	return get_exact_shift(shift_timings_as_per_timestamp, for_timestamp)
+
+
+def get_exact_shift(shifts: List, for_timestamp: datetime) -> Dict:
+	"""Returns the shift details (dict) for the exact shift in which the 'for_timestamp' value falls among multiple shifts"""
+	shift_details = dict()
+	timestamp_list = []
+
+	for shift in shifts:
+		if shift:
+			timestamp_list.extend([shift.actual_start, shift.actual_end])
+		else:
+			timestamp_list.extend([None, None])
+
+	timestamp_index = None
+	for index, timestamp in enumerate(timestamp_list):
+		if not timestamp:
+			continue
+
+		if for_timestamp < timestamp:
+			timestamp_index = index
+		elif for_timestamp == timestamp:
+			# on timestamp boundary
+			if index % 2 == 1:
+				timestamp_index = index
+			else:
+				timestamp_index = index + 1
+
+		if timestamp_index:
+			break
+
+	if timestamp_index and timestamp_index % 2 == 1:
+		shift_details = shifts[int((timestamp_index - 1) / 2)]
+
+	return shift_details
+
+
+def get_shift_details(shift_type_name: str, for_timestamp: datetime = None) -> Dict:
+	"""Returns a Dict containing shift details with the following data:
+	'shift_type' - Object of DocType Shift Type,
+	'start_datetime' - datetime of shift start on given timestamp,
+	'end_datetime' - datetime of shift end on given timestamp,
+	'actual_start' - datetime of shift start after adding 'begin_check_in_before_shift_start_time',
+	'actual_end' - datetime of shift end after adding 'allow_check_out_after_shift_end_time' (None is returned if this is zero)
+
+	:param shift_type_name (str): shift type name for which shift_details are required.
+	:param for_timestamp (datetime, optional): Datetime value of checkin, if not provided considers current datetime
 	"""
 	if not shift_type_name:
-		return None
-	if not for_date:
-		for_date = nowdate()
+		return {}
+
+	if for_timestamp is None:
+		for_timestamp = now_datetime()
+
 	shift_type = frappe.get_doc("Shift Type", shift_type_name)
-	start_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.start_time
-	for_date = (
-		for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date
+	shift_actual_start = shift_type.start_time - timedelta(
+		minutes=shift_type.begin_check_in_before_shift_start_time
 	)
-	end_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.end_time
+
+	if shift_type.start_time > shift_type.end_time:
+		# shift spans accross 2 different days
+		if get_time(for_timestamp.time()) >= get_time(shift_actual_start):
+			# if for_timestamp is greater than start time, it's within the first day
+			start_datetime = datetime.combine(for_timestamp, datetime.min.time()) + shift_type.start_time
+			for_timestamp = for_timestamp + timedelta(days=1)
+			end_datetime = datetime.combine(for_timestamp, datetime.min.time()) + shift_type.end_time
+
+		elif get_time(for_timestamp.time()) < get_time(shift_actual_start):
+			# if for_timestamp is less than start time, it's within the second day
+			end_datetime = datetime.combine(for_timestamp, datetime.min.time()) + shift_type.end_time
+			for_timestamp = for_timestamp + timedelta(days=-1)
+			start_datetime = datetime.combine(for_timestamp, datetime.min.time()) + shift_type.start_time
+	else:
+		# start and end timings fall on the same day
+		start_datetime = datetime.combine(for_timestamp, datetime.min.time()) + shift_type.start_time
+		end_datetime = datetime.combine(for_timestamp, datetime.min.time()) + shift_type.end_time
+
 	actual_start = start_datetime - timedelta(
 		minutes=shift_type.begin_check_in_before_shift_start_time
 	)
@@ -312,34 +513,3 @@
 			"actual_end": actual_end,
 		}
 	)
-
-
-def get_actual_start_end_datetime_of_shift(employee, for_datetime, consider_default_shift=False):
-	"""Takes a datetime and returns the 'actual' start datetime and end datetime of the shift in which the timestamp belongs.
-	Here 'actual' means - taking in to account the "begin_check_in_before_shift_start_time" and "allow_check_out_after_shift_end_time".
-	None is returned if the timestamp is outside any actual shift timings.
-	Shift Details is also returned(current/upcoming i.e. if timestamp not in any actual shift then details of next shift returned)
-	"""
-	actual_shift_start = actual_shift_end = shift_details = None
-	shift_timings_as_per_timestamp = get_employee_shift_timings(
-		employee, for_datetime, consider_default_shift
-	)
-	timestamp_list = []
-	for shift in shift_timings_as_per_timestamp:
-		if shift:
-			timestamp_list.extend([shift.actual_start, shift.actual_end])
-		else:
-			timestamp_list.extend([None, None])
-	timestamp_index = None
-	for index, timestamp in enumerate(timestamp_list):
-		if timestamp and for_datetime <= timestamp:
-			timestamp_index = index
-			break
-	if timestamp_index and timestamp_index % 2 == 1:
-		shift_details = shift_timings_as_per_timestamp[int((timestamp_index - 1) / 2)]
-		actual_shift_start = shift_details.actual_start
-		actual_shift_end = shift_details.actual_end
-	elif timestamp_index:
-		shift_details = shift_timings_as_per_timestamp[int(timestamp_index / 2)]
-
-	return actual_shift_start, actual_shift_end, shift_details
diff --git a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py
index 4a1ec29..0fe9108 100644
--- a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py
@@ -4,16 +4,23 @@
 import unittest
 
 import frappe
-from frappe.utils import add_days, nowdate
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, getdate, nowdate
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.shift_assignment.shift_assignment import OverlappingShiftError
+from erpnext.hr.doctype.shift_type.test_shift_type import make_shift_assignment, setup_shift_type
 
 test_dependencies = ["Shift Type"]
 
 
-class TestShiftAssignment(unittest.TestCase):
+class TestShiftAssignment(FrappeTestCase):
 	def setUp(self):
-		frappe.db.sql("delete from `tabShift Assignment`")
+		frappe.db.delete("Shift Assignment")
+		frappe.db.delete("Shift Type")
 
 	def test_make_shift_assignment(self):
+		setup_shift_type(shift_type="Day Shift")
 		shift_assignment = frappe.get_doc(
 			{
 				"doctype": "Shift Assignment",
@@ -29,7 +36,7 @@
 
 	def test_overlapping_for_ongoing_shift(self):
 		# shift should be Ongoing if Only start_date is present and status = Active
-
+		setup_shift_type(shift_type="Day Shift")
 		shift_assignment_1 = frappe.get_doc(
 			{
 				"doctype": "Shift Assignment",
@@ -54,11 +61,11 @@
 			}
 		)
 
-		self.assertRaises(frappe.ValidationError, shift_assignment.save)
+		self.assertRaises(OverlappingShiftError, shift_assignment.save)
 
 	def test_overlapping_for_fixed_period_shift(self):
 		# shift should is for Fixed period if Only start_date and end_date both are present and status = Active
-
+		setup_shift_type(shift_type="Day Shift")
 		shift_assignment_1 = frappe.get_doc(
 			{
 				"doctype": "Shift Assignment",
@@ -85,4 +92,65 @@
 			}
 		)
 
-		self.assertRaises(frappe.ValidationError, shift_assignment_3.save)
+		self.assertRaises(OverlappingShiftError, shift_assignment_3.save)
+
+	def test_overlapping_for_a_fixed_period_shift_and_ongoing_shift(self):
+		employee = make_employee("test_shift_assignment@example.com", company="_Test Company")
+
+		# shift setup for 8-12
+		shift_type = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="12:00:00")
+		date = getdate()
+		# shift with end date
+		make_shift_assignment(shift_type.name, employee, date, add_days(date, 30))
+
+		# shift setup for 11-15
+		shift_type = setup_shift_type(shift_type="Shift 2", start_time="11:00:00", end_time="15:00:00")
+		date = getdate()
+
+		# shift assignment without end date
+		shift2 = frappe.get_doc(
+			{
+				"doctype": "Shift Assignment",
+				"shift_type": shift_type.name,
+				"company": "_Test Company",
+				"employee": employee,
+				"start_date": date,
+			}
+		)
+		self.assertRaises(OverlappingShiftError, shift2.insert)
+
+	def test_overlap_validation_for_shifts_on_same_day_with_overlapping_timeslots(self):
+		employee = make_employee("test_shift_assignment@example.com", company="_Test Company")
+
+		# shift setup for 8-12
+		shift_type = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="12:00:00")
+		date = getdate()
+		make_shift_assignment(shift_type.name, employee, date)
+
+		# shift setup for 11-15
+		shift_type = setup_shift_type(shift_type="Shift 2", start_time="11:00:00", end_time="15:00:00")
+		date = getdate()
+
+		shift2 = frappe.get_doc(
+			{
+				"doctype": "Shift Assignment",
+				"shift_type": shift_type.name,
+				"company": "_Test Company",
+				"employee": employee,
+				"start_date": date,
+			}
+		)
+		self.assertRaises(OverlappingShiftError, shift2.insert)
+
+	def test_multiple_shift_assignments_for_same_day(self):
+		employee = make_employee("test_shift_assignment@example.com", company="_Test Company")
+
+		# shift setup for 8-12
+		shift_type = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="12:00:00")
+		date = getdate()
+		make_shift_assignment(shift_type.name, employee, date)
+
+		# shift setup for 13-15
+		shift_type = setup_shift_type(shift_type="Shift 2", start_time="13:00:00", end_time="15:00:00")
+		date = getdate()
+		make_shift_assignment(shift_type.name, employee, date)
diff --git a/erpnext/hr/doctype/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py
index 1e3e8ff..2bee240 100644
--- a/erpnext/hr/doctype/shift_request/shift_request.py
+++ b/erpnext/hr/doctype/shift_request/shift_request.py
@@ -5,12 +5,14 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe.utils import formatdate, getdate
+from frappe.query_builder import Criterion
+from frappe.utils import get_link_to_form, getdate
 
+from erpnext.hr.doctype.shift_assignment.shift_assignment import has_overlapping_timings
 from erpnext.hr.utils import share_doc_with_approver, validate_active_employee
 
 
-class OverlapError(frappe.ValidationError):
+class OverlappingShiftRequestError(frappe.ValidationError):
 	pass
 
 
@@ -18,7 +20,7 @@
 	def validate(self):
 		validate_active_employee(self.employee)
 		self.validate_dates()
-		self.validate_shift_request_overlap_dates()
+		self.validate_overlapping_shift_requests()
 		self.validate_approver()
 		self.validate_default_shift()
 
@@ -79,37 +81,60 @@
 		if self.from_date and self.to_date and (getdate(self.to_date) < getdate(self.from_date)):
 			frappe.throw(_("To date cannot be before from date"))
 
-	def validate_shift_request_overlap_dates(self):
+	def validate_overlapping_shift_requests(self):
+		overlapping_dates = self.get_overlapping_dates()
+		if len(overlapping_dates):
+			# if dates are overlapping, check if timings are overlapping, else allow
+			overlapping_timings = has_overlapping_timings(self.shift_type, overlapping_dates[0].shift_type)
+			if overlapping_timings:
+				self.throw_overlap_error(overlapping_dates[0])
+
+	def get_overlapping_dates(self):
 		if not self.name:
 			self.name = "New Shift Request"
 
-		d = frappe.db.sql(
-			"""
-				select
-					name, shift_type, from_date, to_date
-				from `tabShift Request`
-				where employee = %(employee)s and docstatus < 2
-				and ((%(from_date)s >= from_date
-					and %(from_date)s <= to_date) or
-					( %(to_date)s >= from_date
-					and %(to_date)s <= to_date ))
-				and name != %(name)s""",
-			{
-				"employee": self.employee,
-				"shift_type": self.shift_type,
-				"from_date": self.from_date,
-				"to_date": self.to_date,
-				"name": self.name,
-			},
-			as_dict=1,
+		shift = frappe.qb.DocType("Shift Request")
+		query = (
+			frappe.qb.from_(shift)
+			.select(shift.name, shift.shift_type)
+			.where((shift.employee == self.employee) & (shift.docstatus < 2) & (shift.name != self.name))
 		)
 
-		for date_overlap in d:
-			if date_overlap["name"]:
-				self.throw_overlap_error(date_overlap)
+		if self.to_date:
+			query = query.where(
+				Criterion.any(
+					[
+						Criterion.any(
+							[
+								shift.to_date.isnull(),
+								((self.from_date >= shift.from_date) & (self.from_date <= shift.to_date)),
+							]
+						),
+						Criterion.any(
+							[
+								((self.to_date >= shift.from_date) & (self.to_date <= shift.to_date)),
+								shift.from_date.between(self.from_date, self.to_date),
+							]
+						),
+					]
+				)
+			)
+		else:
+			query = query.where(
+				shift.to_date.isnull()
+				| ((self.from_date >= shift.from_date) & (self.from_date <= shift.to_date))
+			)
 
-	def throw_overlap_error(self, d):
-		msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(
-			self.employee, d["shift_type"], formatdate(d["from_date"]), formatdate(d["to_date"])
-		) + """ <b><a href="/app/Form/Shift Request/{0}">{0}</a></b>""".format(d["name"])
-		frappe.throw(msg, OverlapError)
+		return query.run(as_dict=True)
+
+	def throw_overlap_error(self, shift_details):
+		shift_details = frappe._dict(shift_details)
+		msg = _(
+			"Employee {0} has already applied for Shift {1}: {2} that overlaps within this period"
+		).format(
+			frappe.bold(self.employee),
+			frappe.bold(shift_details.shift_type),
+			get_link_to_form("Shift Request", shift_details.name),
+		)
+
+		frappe.throw(msg, title=_("Overlapping Shift Requests"), exc=OverlappingShiftRequestError)
diff --git a/erpnext/hr/doctype/shift_request/test_shift_request.py b/erpnext/hr/doctype/shift_request/test_shift_request.py
index b4f5177..c47418c 100644
--- a/erpnext/hr/doctype/shift_request/test_shift_request.py
+++ b/erpnext/hr/doctype/shift_request/test_shift_request.py
@@ -4,23 +4,24 @@
 import unittest
 
 import frappe
+from frappe.tests.utils import FrappeTestCase
 from frappe.utils import add_days, nowdate
 
 from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.shift_request.shift_request import OverlappingShiftRequestError
+from erpnext.hr.doctype.shift_type.test_shift_type import setup_shift_type
 
 test_dependencies = ["Shift Type"]
 
 
-class TestShiftRequest(unittest.TestCase):
+class TestShiftRequest(FrappeTestCase):
 	def setUp(self):
-		for doctype in ["Shift Request", "Shift Assignment"]:
-			frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype))
-
-	def tearDown(self):
-		frappe.db.rollback()
+		for doctype in ["Shift Request", "Shift Assignment", "Shift Type"]:
+			frappe.db.delete(doctype)
 
 	def test_make_shift_request(self):
 		"Test creation/updation of Shift Assignment from Shift Request."
+		setup_shift_type(shift_type="Day Shift")
 		department = frappe.get_value("Employee", "_T-Employee-00001", "department")
 		set_shift_approver(department)
 		approver = frappe.db.sql(
@@ -48,6 +49,7 @@
 		self.assertEqual(shift_assignment_docstatus, 2)
 
 	def test_shift_request_approver_perms(self):
+		setup_shift_type(shift_type="Day Shift")
 		employee = frappe.get_doc("Employee", "_T-Employee-00001")
 		user = "test_approver_perm_emp@example.com"
 		make_employee(user, "_Test Company")
@@ -87,6 +89,145 @@
 		employee.shift_request_approver = ""
 		employee.save()
 
+	def test_overlap_for_request_without_to_date(self):
+		# shift should be Ongoing if Only from_date is present
+		user = "test_shift_request@example.com"
+		employee = make_employee(user, company="_Test Company", shift_request_approver=user)
+		setup_shift_type(shift_type="Day Shift")
+
+		shift_request = frappe.get_doc(
+			{
+				"doctype": "Shift Request",
+				"shift_type": "Day Shift",
+				"company": "_Test Company",
+				"employee": employee,
+				"from_date": nowdate(),
+				"approver": user,
+				"status": "Approved",
+			}
+		).submit()
+
+		shift_request = frappe.get_doc(
+			{
+				"doctype": "Shift Request",
+				"shift_type": "Day Shift",
+				"company": "_Test Company",
+				"employee": employee,
+				"from_date": add_days(nowdate(), 2),
+				"approver": user,
+				"status": "Approved",
+			}
+		)
+
+		self.assertRaises(OverlappingShiftRequestError, shift_request.save)
+
+	def test_overlap_for_request_with_from_and_to_dates(self):
+		user = "test_shift_request@example.com"
+		employee = make_employee(user, company="_Test Company", shift_request_approver=user)
+		setup_shift_type(shift_type="Day Shift")
+
+		shift_request = frappe.get_doc(
+			{
+				"doctype": "Shift Request",
+				"shift_type": "Day Shift",
+				"company": "_Test Company",
+				"employee": employee,
+				"from_date": nowdate(),
+				"to_date": add_days(nowdate(), 30),
+				"approver": user,
+				"status": "Approved",
+			}
+		).submit()
+
+		shift_request = frappe.get_doc(
+			{
+				"doctype": "Shift Request",
+				"shift_type": "Day Shift",
+				"company": "_Test Company",
+				"employee": employee,
+				"from_date": add_days(nowdate(), 10),
+				"to_date": add_days(nowdate(), 35),
+				"approver": user,
+				"status": "Approved",
+			}
+		)
+
+		self.assertRaises(OverlappingShiftRequestError, shift_request.save)
+
+	def test_overlapping_for_a_fixed_period_shift_and_ongoing_shift(self):
+		user = "test_shift_request@example.com"
+		employee = make_employee(user, company="_Test Company", shift_request_approver=user)
+
+		# shift setup for 8-12
+		shift_type = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="12:00:00")
+		date = nowdate()
+
+		# shift with end date
+		frappe.get_doc(
+			{
+				"doctype": "Shift Request",
+				"shift_type": shift_type.name,
+				"company": "_Test Company",
+				"employee": employee,
+				"from_date": date,
+				"to_date": add_days(date, 30),
+				"approver": user,
+				"status": "Approved",
+			}
+		).submit()
+
+		# shift setup for 11-15
+		shift_type = setup_shift_type(shift_type="Shift 2", start_time="11:00:00", end_time="15:00:00")
+		shift2 = frappe.get_doc(
+			{
+				"doctype": "Shift Request",
+				"shift_type": shift_type.name,
+				"company": "_Test Company",
+				"employee": employee,
+				"from_date": date,
+				"approver": user,
+				"status": "Approved",
+			}
+		)
+
+		self.assertRaises(OverlappingShiftRequestError, shift2.insert)
+
+	def test_allow_non_overlapping_shift_requests_for_same_day(self):
+		user = "test_shift_request@example.com"
+		employee = make_employee(user, company="_Test Company", shift_request_approver=user)
+
+		# shift setup for 8-12
+		shift_type = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="12:00:00")
+		date = nowdate()
+
+		# shift with end date
+		frappe.get_doc(
+			{
+				"doctype": "Shift Request",
+				"shift_type": shift_type.name,
+				"company": "_Test Company",
+				"employee": employee,
+				"from_date": date,
+				"to_date": add_days(date, 30),
+				"approver": user,
+				"status": "Approved",
+			}
+		).submit()
+
+		# shift setup for 13-15
+		shift_type = setup_shift_type(shift_type="Shift 2", start_time="13:00:00", end_time="15:00:00")
+		frappe.get_doc(
+			{
+				"doctype": "Shift Request",
+				"shift_type": shift_type.name,
+				"company": "_Test Company",
+				"employee": employee,
+				"from_date": date,
+				"approver": user,
+				"status": "Approved",
+			}
+		).submit()
+
 
 def set_shift_approver(department):
 	department_doc = frappe.get_doc("Department", department)
diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py
index 3f5cb22..5e214cf 100644
--- a/erpnext/hr/doctype/shift_type/shift_type.py
+++ b/erpnext/hr/doctype/shift_type/shift_type.py
@@ -3,21 +3,23 @@
 
 
 import itertools
-from datetime import timedelta
+from datetime import datetime, timedelta
 
 import frappe
 from frappe.model.document import Document
-from frappe.utils import cint, get_datetime, getdate
+from frappe.utils import cint, get_datetime, get_time, getdate
 
+from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange
 from erpnext.hr.doctype.attendance.attendance import mark_attendance
 from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
 from erpnext.hr.doctype.employee_checkin.employee_checkin import (
 	calculate_working_hours,
 	mark_attendance_and_link_log,
 )
+from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
 from erpnext.hr.doctype.shift_assignment.shift_assignment import (
-	get_actual_start_end_datetime_of_shift,
 	get_employee_shift,
+	get_shift_details,
 )
 
 
@@ -30,8 +32,9 @@
 			or not self.last_sync_of_checkin
 		):
 			return
+
 		filters = {
-			"skip_auto_attendance": "0",
+			"skip_auto_attendance": 0,
 			"attendance": ("is", "not set"),
 			"time": (">=", self.process_attendance_after),
 			"shift_actual_end": ("<", self.last_sync_of_checkin),
@@ -40,6 +43,7 @@
 		logs = frappe.db.get_list(
 			"Employee Checkin", fields="*", filters=filters, order_by="employee,time"
 		)
+
 		for key, group in itertools.groupby(
 			logs, key=lambda x: (x["employee"], x["shift_actual_start"])
 		):
@@ -52,6 +56,7 @@
 				in_time,
 				out_time,
 			) = self.get_attendance(single_shift_logs)
+
 			mark_attendance_and_link_log(
 				single_shift_logs,
 				attendance_status,
@@ -63,15 +68,16 @@
 				out_time,
 				self.name,
 			)
+
 		for employee in self.get_assigned_employee(self.process_attendance_after, True):
 			self.mark_absent_for_dates_with_no_attendance(employee)
 
 	def get_attendance(self, logs):
 		"""Return attendance_status, working_hours, late_entry, early_exit, in_time, out_time
 		for a set of logs belonging to a single shift.
-		Assumtion:
-		        1. These logs belongs to an single shift, single employee and is not in a holiday date.
-		        2. Logs are in chronological order
+		Assumptions:
+		1. These logs belongs to a single shift, single employee and it's not in a holiday date.
+		2. Logs are in chronological order
 		"""
 		late_entry = early_exit = False
 		total_working_hours, in_time, out_time = calculate_working_hours(
@@ -92,38 +98,67 @@
 			early_exit = True
 
 		if (
-			self.working_hours_threshold_for_absent
-			and total_working_hours < self.working_hours_threshold_for_absent
-		):
-			return "Absent", total_working_hours, late_entry, early_exit, in_time, out_time
-		if (
 			self.working_hours_threshold_for_half_day
 			and total_working_hours < self.working_hours_threshold_for_half_day
 		):
 			return "Half Day", total_working_hours, late_entry, early_exit, in_time, out_time
+		if (
+			self.working_hours_threshold_for_absent
+			and total_working_hours < self.working_hours_threshold_for_absent
+		):
+			return "Absent", total_working_hours, late_entry, early_exit, in_time, out_time
 		return "Present", total_working_hours, late_entry, early_exit, in_time, out_time
 
 	def mark_absent_for_dates_with_no_attendance(self, employee):
 		"""Marks Absents for the given employee on working days in this shift which have no attendance marked.
 		The Absent is marked starting from 'process_attendance_after' or employee creation date.
 		"""
+		start_date, end_date = self.get_start_and_end_dates(employee)
+
+		# no shift assignment found, no need to process absent attendance records
+		if start_date is None:
+			return
+
+		holiday_list_name = self.holiday_list
+		if not holiday_list_name:
+			holiday_list_name = get_holiday_list_for_employee(employee, False)
+
+		start_time = get_time(self.start_time)
+
+		for date in daterange(getdate(start_date), getdate(end_date)):
+			if is_holiday(holiday_list_name, date):
+				# skip marking absent on a holiday
+				continue
+
+			timestamp = datetime.combine(date, start_time)
+			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)
+
+	def get_start_and_end_dates(self, employee):
+		"""Returns start and end dates for checking attendance and marking absent
+		return: start date = max of `process_attendance_after` and DOJ
+		return: end date = min of shift before `last_sync_of_checkin` and Relieving Date
+		"""
 		date_of_joining, relieving_date, employee_creation = frappe.db.get_value(
 			"Employee", employee, ["date_of_joining", "relieving_date", "creation"]
 		)
+
 		if not date_of_joining:
 			date_of_joining = employee_creation.date()
+
 		start_date = max(getdate(self.process_attendance_after), date_of_joining)
-		actual_shift_datetime = get_actual_start_end_datetime_of_shift(
-			employee, get_datetime(self.last_sync_of_checkin), True
-		)
+		end_date = None
+
+		shift_details = get_shift_details(self.name, get_datetime(self.last_sync_of_checkin))
 		last_shift_time = (
-			actual_shift_datetime[0]
-			if actual_shift_datetime[0]
-			else get_datetime(self.last_sync_of_checkin)
+			shift_details.actual_start if shift_details else get_datetime(self.last_sync_of_checkin)
 		)
-		prev_shift = get_employee_shift(
-			employee, last_shift_time.date() - timedelta(days=1), True, "reverse"
-		)
+
+		# check if shift is found for 1 day before the last sync of checkin
+		# absentees are auto-marked 1 day after the shift to wait for any manual attendance records
+		prev_shift = get_employee_shift(employee, last_shift_time - timedelta(days=1), True, "reverse")
 		if prev_shift:
 			end_date = (
 				min(prev_shift.start_datetime.date(), relieving_date)
@@ -131,28 +166,21 @@
 				else prev_shift.start_datetime.date()
 			)
 		else:
-			return
-		holiday_list_name = self.holiday_list
-		if not holiday_list_name:
-			holiday_list_name = get_holiday_list_for_employee(employee, False)
-		dates = get_filtered_date_list(employee, start_date, end_date, holiday_list=holiday_list_name)
-		for date in dates:
-			shift_details = get_employee_shift(employee, date, True)
-			if shift_details and shift_details.shift_type.name == self.name:
-				mark_attendance(employee, date, "Absent", self.name)
+			# no shift found
+			return None, None
+		return start_date, end_date
 
 	def get_assigned_employee(self, from_date=None, consider_default_shift=False):
-		filters = {"start_date": (">", from_date), "shift_type": self.name, "docstatus": "1"}
-		if not from_date:
-			del filters["start_date"]
+		filters = {"shift_type": self.name, "docstatus": "1"}
+		if from_date:
+			filters["start_date"] = (">", from_date)
 
-		assigned_employees = frappe.get_all("Shift Assignment", "employee", filters, as_list=True)
-		assigned_employees = [x[0] for x in assigned_employees]
+		assigned_employees = frappe.get_all("Shift Assignment", filters=filters, pluck="employee")
 
 		if consider_default_shift:
 			filters = {"default_shift": self.name, "status": ["!=", "Inactive"]}
-			default_shift_employees = frappe.get_all("Employee", "name", filters, as_list=True)
-			default_shift_employees = [x[0] for x in default_shift_employees]
+			default_shift_employees = frappe.get_all("Employee", filters=filters, pluck="name")
+
 			return list(set(assigned_employees + default_shift_employees))
 		return assigned_employees
 
@@ -162,42 +190,3 @@
 	for shift in shift_list:
 		doc = frappe.get_doc("Shift Type", shift[0])
 		doc.process_auto_attendance()
-
-
-def get_filtered_date_list(
-	employee, start_date, end_date, filter_attendance=True, holiday_list=None
-):
-	"""Returns a list of dates after removing the dates with attendance and holidays"""
-	base_dates_query = """select adddate(%(start_date)s, t2.i*100 + t1.i*10 + t0.i) selected_date from
-		(select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t0,
-		(select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t1,
-		(select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t2"""
-	condition_query = ""
-	if filter_attendance:
-		condition_query += """ and a.selected_date not in (
-			select attendance_date from `tabAttendance`
-			where docstatus = 1 and employee = %(employee)s
-			and attendance_date between %(start_date)s and %(end_date)s)"""
-	if holiday_list:
-		condition_query += """ and a.selected_date not in (
-			select holiday_date from `tabHoliday` where parenttype = 'Holiday List' and
-    		parentfield = 'holidays' and parent = %(holiday_list)s
-    		and holiday_date between %(start_date)s and %(end_date)s)"""
-
-	dates = frappe.db.sql(
-		"""select * from
-		({base_dates_query}) as a
-		where a.selected_date <= %(end_date)s {condition_query}
-		""".format(
-			base_dates_query=base_dates_query, condition_query=condition_query
-		),
-		{
-			"employee": employee,
-			"start_date": start_date,
-			"end_date": end_date,
-			"holiday_list": holiday_list,
-		},
-		as_list=True,
-	)
-
-	return [getdate(date[0]) for date in dates]
diff --git a/erpnext/hr/doctype/shift_type/test_shift_type.py b/erpnext/hr/doctype/shift_type/test_shift_type.py
index 7d2f29c..0d75292 100644
--- a/erpnext/hr/doctype/shift_type/test_shift_type.py
+++ b/erpnext/hr/doctype/shift_type/test_shift_type.py
@@ -2,7 +2,381 @@
 # See license.txt
 
 import unittest
+from datetime import datetime, timedelta
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, get_time, get_year_ending, get_year_start, getdate, now_datetime
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
+from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
 
 
-class TestShiftType(unittest.TestCase):
-	pass
+class TestShiftType(FrappeTestCase):
+	def setUp(self):
+		frappe.db.delete("Shift Type")
+		frappe.db.delete("Shift Assignment")
+		frappe.db.delete("Employee Checkin")
+		frappe.db.delete("Attendance")
+
+		from_date = get_year_start(getdate())
+		to_date = get_year_ending(getdate())
+		self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
+
+	def test_mark_attendance(self):
+		from erpnext.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
+
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+
+		shift_type = setup_shift_type()
+		date = getdate()
+		make_shift_assignment(shift_type.name, employee, date)
+
+		timestamp = datetime.combine(date, get_time("08:00:00"))
+		log_in = make_checkin(employee, timestamp)
+		self.assertEqual(log_in.shift, shift_type.name)
+
+		timestamp = datetime.combine(date, get_time("12:00:00"))
+		log_out = make_checkin(employee, timestamp)
+		self.assertEqual(log_out.shift, shift_type.name)
+
+		shift_type.process_auto_attendance()
+
+		attendance = frappe.db.get_value(
+			"Attendance", {"shift": shift_type.name}, ["status", "name"], as_dict=True
+		)
+		self.assertEqual(attendance.status, "Present")
+
+	def test_entry_and_exit_grace(self):
+		from erpnext.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
+
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+
+		# doesn't mark late entry until 60 mins after shift start i.e. till 9
+		# doesn't mark late entry until 60 mins before shift end i.e. 11
+		shift_type = setup_shift_type(
+			enable_entry_grace_period=1,
+			enable_exit_grace_period=1,
+			late_entry_grace_period=60,
+			early_exit_grace_period=60,
+		)
+		date = getdate()
+		make_shift_assignment(shift_type.name, employee, date)
+
+		timestamp = datetime.combine(date, get_time("09:30:00"))
+		log_in = make_checkin(employee, timestamp)
+		self.assertEqual(log_in.shift, shift_type.name)
+
+		timestamp = datetime.combine(date, get_time("10:30:00"))
+		log_out = make_checkin(employee, timestamp)
+		self.assertEqual(log_out.shift, shift_type.name)
+
+		shift_type.process_auto_attendance()
+
+		attendance = frappe.db.get_value(
+			"Attendance",
+			{"shift": shift_type.name},
+			["status", "name", "late_entry", "early_exit"],
+			as_dict=True,
+		)
+		self.assertEqual(attendance.status, "Present")
+		self.assertEqual(attendance.late_entry, 1)
+		self.assertEqual(attendance.early_exit, 1)
+
+	def test_working_hours_threshold_for_half_day(self):
+		from erpnext.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
+
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+		shift_type = setup_shift_type(shift_type="Half Day Test", working_hours_threshold_for_half_day=2)
+		date = getdate()
+		make_shift_assignment(shift_type.name, employee, date)
+
+		timestamp = datetime.combine(date, get_time("08:00:00"))
+		log_in = make_checkin(employee, timestamp)
+		self.assertEqual(log_in.shift, shift_type.name)
+
+		timestamp = datetime.combine(date, get_time("09:30:00"))
+		log_out = make_checkin(employee, timestamp)
+		self.assertEqual(log_out.shift, shift_type.name)
+
+		shift_type.process_auto_attendance()
+
+		attendance = frappe.db.get_value(
+			"Attendance", {"shift": shift_type.name}, ["status", "working_hours"], as_dict=True
+		)
+		self.assertEqual(attendance.status, "Half Day")
+		self.assertEqual(attendance.working_hours, 1.5)
+
+	def test_working_hours_threshold_for_absent(self):
+		from erpnext.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
+
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+		shift_type = setup_shift_type(shift_type="Absent Test", working_hours_threshold_for_absent=2)
+		date = getdate()
+		make_shift_assignment(shift_type.name, employee, date)
+
+		timestamp = datetime.combine(date, get_time("08:00:00"))
+		log_in = make_checkin(employee, timestamp)
+		self.assertEqual(log_in.shift, shift_type.name)
+
+		timestamp = datetime.combine(date, get_time("09:30:00"))
+		log_out = make_checkin(employee, timestamp)
+		self.assertEqual(log_out.shift, shift_type.name)
+
+		shift_type.process_auto_attendance()
+
+		attendance = frappe.db.get_value(
+			"Attendance", {"shift": shift_type.name}, ["status", "working_hours"], as_dict=True
+		)
+		self.assertEqual(attendance.status, "Absent")
+		self.assertEqual(attendance.working_hours, 1.5)
+
+	def test_working_hours_threshold_for_absent_and_half_day_1(self):
+		# considers half day over absent
+		from erpnext.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
+
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+		shift_type = setup_shift_type(
+			shift_type="Half Day + Absent Test",
+			working_hours_threshold_for_half_day=1,
+			working_hours_threshold_for_absent=2,
+		)
+		date = getdate()
+		make_shift_assignment(shift_type.name, employee, date)
+
+		timestamp = datetime.combine(date, get_time("08:00:00"))
+		log_in = make_checkin(employee, timestamp)
+		self.assertEqual(log_in.shift, shift_type.name)
+
+		timestamp = datetime.combine(date, get_time("08:45:00"))
+		log_out = make_checkin(employee, timestamp)
+		self.assertEqual(log_out.shift, shift_type.name)
+
+		shift_type.process_auto_attendance()
+
+		attendance = frappe.db.get_value(
+			"Attendance", {"shift": shift_type.name}, ["status", "working_hours"], as_dict=True
+		)
+		self.assertEqual(attendance.status, "Half Day")
+		self.assertEqual(attendance.working_hours, 0.75)
+
+	def test_working_hours_threshold_for_absent_and_half_day_2(self):
+		# considers absent over half day
+		from erpnext.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
+
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+		shift_type = setup_shift_type(
+			shift_type="Half Day + Absent Test",
+			working_hours_threshold_for_half_day=1,
+			working_hours_threshold_for_absent=2,
+		)
+		date = getdate()
+		make_shift_assignment(shift_type.name, employee, date)
+
+		timestamp = datetime.combine(date, get_time("08:00:00"))
+		log_in = make_checkin(employee, timestamp)
+		self.assertEqual(log_in.shift, shift_type.name)
+
+		timestamp = datetime.combine(date, get_time("09:30:00"))
+		log_out = make_checkin(employee, timestamp)
+		self.assertEqual(log_out.shift, shift_type.name)
+
+		shift_type.process_auto_attendance()
+
+		attendance = frappe.db.get_value("Attendance", {"shift": shift_type.name}, "status")
+		self.assertEqual(attendance, "Absent")
+
+	def test_mark_absent_for_dates_with_no_attendance(self):
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+		shift_type = setup_shift_type(shift_type="Test Absent with no Attendance")
+
+		# absentees are auto-marked one day after to wait for any manual attendance records
+		date = add_days(getdate(), -1)
+		make_shift_assignment(shift_type.name, employee, date)
+
+		shift_type.process_auto_attendance()
+
+		attendance = frappe.db.get_value(
+			"Attendance", {"attendance_date": date, "employee": employee}, "status"
+		)
+		self.assertEqual(attendance, "Absent")
+
+	@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+	def test_skip_marking_absent_on_a_holiday(self):
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+		shift_type = setup_shift_type(shift_type="Test Absent with no Attendance")
+		shift_type.holiday_list = None
+		shift_type.save()
+
+		# should not mark any attendance if no shift assignment is created
+		shift_type.process_auto_attendance()
+		attendance = frappe.db.get_value("Attendance", {"employee": employee}, "status")
+		self.assertIsNone(attendance)
+
+		first_sunday = get_first_sunday(self.holiday_list, for_date=getdate())
+		make_shift_assignment(shift_type.name, employee, first_sunday)
+
+		shift_type.process_auto_attendance()
+
+		attendance = frappe.db.get_value(
+			"Attendance", {"attendance_date": first_sunday, "employee": employee}, "status"
+		)
+		self.assertIsNone(attendance)
+
+	def test_get_start_and_end_dates(self):
+		date = getdate()
+
+		doj = add_days(date, -30)
+		relieving_date = add_days(date, -5)
+		employee = make_employee(
+			"test_employee_dates@example.com",
+			company="_Test Company",
+			date_of_joining=doj,
+			relieving_date=relieving_date,
+		)
+		shift_type = setup_shift_type(
+			shift_type="Test Absent with no Attendance", process_attendance_after=add_days(doj, 2)
+		)
+
+		make_shift_assignment(shift_type.name, employee, add_days(date, -25))
+
+		shift_type.process_auto_attendance()
+
+		# should not mark absent before shift assignment/process attendance after date
+		attendance = frappe.db.get_value(
+			"Attendance", {"attendance_date": doj, "employee": employee}, "name"
+		)
+		self.assertIsNone(attendance)
+
+		# mark absent on Relieving Date
+		attendance = frappe.db.get_value(
+			"Attendance", {"attendance_date": relieving_date, "employee": employee}, "status"
+		)
+		self.assertEquals(attendance, "Absent")
+
+		# should not mark absent after Relieving Date
+		attendance = frappe.db.get_value(
+			"Attendance", {"attendance_date": add_days(relieving_date, 1), "employee": employee}, "name"
+		)
+		self.assertIsNone(attendance)
+
+	def test_skip_auto_attendance_for_duplicate_record(self):
+		# Skip auto attendance in case of duplicate attendance record
+		from erpnext.hr.doctype.attendance.attendance import mark_attendance
+		from erpnext.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
+
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+
+		shift_type = setup_shift_type()
+		date = getdate()
+
+		# mark attendance
+		mark_attendance(employee, date, "Present")
+		make_shift_assignment(shift_type.name, employee, date)
+
+		timestamp = datetime.combine(date, get_time("08:00:00"))
+		log_in = make_checkin(employee, timestamp)
+		self.assertEqual(log_in.shift, shift_type.name)
+
+		timestamp = datetime.combine(date, get_time("12:00:00"))
+		log_out = make_checkin(employee, timestamp)
+		self.assertEqual(log_out.shift, shift_type.name)
+
+		# auto attendance should skip marking
+		shift_type.process_auto_attendance()
+
+		log_in.reload()
+		log_out.reload()
+		self.assertEqual(log_in.skip_auto_attendance, 1)
+		self.assertEqual(log_out.skip_auto_attendance, 1)
+
+	def test_skip_auto_attendance_for_overlapping_shift(self):
+		# Skip auto attendance in case of overlapping shift attendance record
+		# this case won't occur in case of shift assignment, since it will not allow overlapping shifts to be assigned
+		# can happen if manual attendance records are created
+		from erpnext.hr.doctype.attendance.attendance import mark_attendance
+		from erpnext.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
+
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+		shift_1 = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="10:00:00")
+		shift_2 = setup_shift_type(shift_type="Shift 2", start_time="09:30:00", end_time="11:00:00")
+
+		date = getdate()
+
+		# mark attendance
+		mark_attendance(employee, date, "Present", shift=shift_1.name)
+		make_shift_assignment(shift_2.name, employee, date)
+
+		timestamp = datetime.combine(date, get_time("09:30:00"))
+		log_in = make_checkin(employee, timestamp)
+		self.assertEqual(log_in.shift, shift_2.name)
+
+		timestamp = datetime.combine(date, get_time("11:00:00"))
+		log_out = make_checkin(employee, timestamp)
+		self.assertEqual(log_out.shift, shift_2.name)
+
+		# auto attendance should be skipped for shift 2
+		# since it is already marked for overlapping shift 1
+		shift_2.process_auto_attendance()
+
+		log_in.reload()
+		log_out.reload()
+		self.assertEqual(log_in.skip_auto_attendance, 1)
+		self.assertEqual(log_out.skip_auto_attendance, 1)
+
+
+def setup_shift_type(**args):
+	args = frappe._dict(args)
+	date = getdate()
+
+	shift_type = frappe.get_doc(
+		{
+			"doctype": "Shift Type",
+			"__newname": args.shift_type or "_Test Shift",
+			"start_time": "08:00:00",
+			"end_time": "12:00:00",
+			"enable_auto_attendance": 1,
+			"determine_check_in_and_check_out": "Alternating entries as IN and OUT during the same shift",
+			"working_hours_calculation_based_on": "First Check-in and Last Check-out",
+			"begin_check_in_before_shift_start_time": 60,
+			"allow_check_out_after_shift_end_time": 60,
+			"process_attendance_after": add_days(date, -2),
+			"last_sync_of_checkin": now_datetime() + timedelta(days=1),
+		}
+	)
+
+	holiday_list = "Employee Checkin Test Holiday List"
+	if not frappe.db.exists("Holiday List", "Employee Checkin Test Holiday List"):
+		holiday_list = frappe.get_doc(
+			{
+				"doctype": "Holiday List",
+				"holiday_list_name": "Employee Checkin Test Holiday List",
+				"from_date": get_year_start(date),
+				"to_date": get_year_ending(date),
+			}
+		).insert()
+		holiday_list = holiday_list.name
+
+	shift_type.holiday_list = holiday_list
+	shift_type.update(args)
+	shift_type.save()
+
+	return shift_type
+
+
+def make_shift_assignment(shift_type, employee, start_date, end_date=None):
+	shift_assignment = frappe.get_doc(
+		{
+			"doctype": "Shift Assignment",
+			"shift_type": shift_type,
+			"company": "_Test Company",
+			"employee": employee,
+			"start_date": start_date,
+			"end_date": end_date,
+		}
+	).insert()
+	shift_assignment.submit()
+
+	return shift_assignment
diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
index 93a493c..ce7e50f 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
@@ -91,8 +91,7 @@
 		) > flt(parent_plan_details[0].total_estimated_cost):
 			frappe.throw(
 				_(
-					"You can only plan for upto {0} vacancies and budget {1} \
-				for {2} as per staffing plan {3} for parent company {4}."
+					"You can only plan for upto {0} vacancies and budget {1} for {2} as per staffing plan {3} for parent company {4}."
 				).format(
 					cint(parent_plan_details[0].vacancies),
 					parent_plan_details[0].total_estimated_cost,
@@ -128,8 +127,7 @@
 		):
 			frappe.throw(
 				_(
-					"{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
-				You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}."
+					"{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}."
 				).format(
 					cint(all_sibling_details.vacancies),
 					all_sibling_details.total_estimated_cost,
@@ -162,8 +160,7 @@
 		):
 			frappe.throw(
 				_(
-					"Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
-				Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies"
+					"Subsidiary companies have already planned for {1} vacancies at a budget of {2}. Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies"
 				).format(
 					self.company,
 					cint(children_details.vacancies),
diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js
index 42f7cdb..6f4bbd5 100644
--- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js
+++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js
@@ -66,8 +66,7 @@
 			"Default": 0,
 		}
 	],
-
-	"onload": function() {
+	onload: function() {
 		return  frappe.call({
 			method: "erpnext.hr.report.monthly_attendance_sheet.monthly_attendance_sheet.get_attendance_years",
 			callback: function(r) {
@@ -78,5 +77,25 @@
 				year_filter.set_input(year_filter.df.default);
 			}
 		});
+	},
+	formatter: function(value, row, column, data, default_formatter) {
+		value = default_formatter(value, row, column, data);
+		const summarized_view = frappe.query_report.get_filter_value('summarized_view');
+		const group_by = frappe.query_report.get_filter_value('group_by');
+
+		if (!summarized_view) {
+			if ((group_by && column.colIndex > 3) || (!group_by && column.colIndex > 2)) {
+				if (value == 'P' || value == 'WFH')
+					value = "<span style='color:green'>" + value + "</span>";
+				else if (value == 'A')
+					value = "<span style='color:red'>" + value + "</span>";
+				else if (value == 'HD')
+					value = "<span style='color:orange'>" + value + "</span>";
+				else if (value == 'L')
+					value = "<span style='color:#318AD8'>" + value + "</span>";
+			}
+		}
+
+		return value;
 	}
 }
diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
index 8ea4989..efd2d38 100644
--- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
+++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
@@ -3,365 +3,618 @@
 
 
 from calendar import monthrange
+from itertools import groupby
+from typing import Dict, List, Optional, Tuple
 
 import frappe
-from frappe import _, msgprint
+from frappe import _
+from frappe.query_builder.functions import Count, Extract, Sum
 from frappe.utils import cint, cstr, getdate
 
+Filters = frappe._dict
+
 status_map = {
+	"Present": "P",
 	"Absent": "A",
 	"Half Day": "HD",
-	"Holiday": "<b>H</b>",
-	"Weekly Off": "<b>WO</b>",
-	"On Leave": "L",
-	"Present": "P",
 	"Work From Home": "WFH",
+	"On Leave": "L",
+	"Holiday": "H",
+	"Weekly Off": "WO",
 }
 
 day_abbr = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
 
 
-def execute(filters=None):
-	if not filters:
-		filters = {}
+def execute(filters: Optional[Filters] = None) -> Tuple:
+	filters = frappe._dict(filters or {})
 
-	if filters.hide_year_field == 1:
-		filters.year = 2020
+	if not (filters.month and filters.year):
+		frappe.throw(_("Please select month and year."))
 
-	conditions, filters = get_conditions(filters)
-	columns, days = get_columns(filters)
-	att_map = get_attendance_list(conditions, filters)
-	if not att_map:
+	attendance_map = get_attendance_map(filters)
+	if not attendance_map:
+		frappe.msgprint(_("No attendance records found."), alert=True, indicator="orange")
+		return [], [], None, None
+
+	columns = get_columns(filters)
+	data = get_data(filters, attendance_map)
+
+	if not data:
+		frappe.msgprint(
+			_("No attendance records found for this criteria."), alert=True, indicator="orange"
+		)
 		return columns, [], None, None
 
-	if filters.group_by:
-		emp_map, group_by_parameters = get_employee_details(filters.group_by, filters.company)
-		holiday_list = []
-		for parameter in group_by_parameters:
-			h_list = [
-				emp_map[parameter][d]["holiday_list"]
-				for d in emp_map[parameter]
-				if emp_map[parameter][d]["holiday_list"]
-			]
-			holiday_list += h_list
-	else:
-		emp_map = get_employee_details(filters.group_by, filters.company)
-		holiday_list = [emp_map[d]["holiday_list"] for d in emp_map if emp_map[d]["holiday_list"]]
+	message = get_message() if not filters.summarized_view else ""
+	chart = get_chart_data(attendance_map, filters)
 
-	default_holiday_list = frappe.get_cached_value(
-		"Company", filters.get("company"), "default_holiday_list"
-	)
-	holiday_list.append(default_holiday_list)
-	holiday_list = list(set(holiday_list))
-	holiday_map = get_holiday(holiday_list, filters["month"])
-
-	data = []
-
-	leave_types = frappe.db.get_list("Leave Type")
-	leave_list = None
-	if filters.summarized_view:
-		leave_list = [d.name + ":Float:120" for d in leave_types]
-		columns.extend(leave_list)
-		columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"])
-
-	if filters.group_by:
-		emp_att_map = {}
-		for parameter in group_by_parameters:
-			emp_map_set = set([key for key in emp_map[parameter].keys()])
-			att_map_set = set([key for key in att_map.keys()])
-			if att_map_set & emp_map_set:
-				parameter_row = ["<b>" + parameter + "</b>"] + [
-					"" for day in range(filters["total_days_in_month"] + 2)
-				]
-				data.append(parameter_row)
-				record, emp_att_data = add_data(
-					emp_map[parameter],
-					att_map,
-					filters,
-					holiday_map,
-					conditions,
-					default_holiday_list,
-					leave_types=leave_types,
-				)
-				emp_att_map.update(emp_att_data)
-				data += record
-	else:
-		record, emp_att_map = add_data(
-			emp_map,
-			att_map,
-			filters,
-			holiday_map,
-			conditions,
-			default_holiday_list,
-			leave_types=leave_types,
-		)
-		data += record
-
-	chart_data = get_chart_data(emp_att_map, days)
-
-	return columns, data, None, chart_data
+	return columns, data, message, chart
 
 
-def get_chart_data(emp_att_map, days):
-	labels = []
-	datasets = [
-		{"name": "Absent", "values": []},
-		{"name": "Present", "values": []},
-		{"name": "Leave", "values": []},
-	]
-	for idx, day in enumerate(days, start=0):
-		p = day.replace("::65", "")
-		labels.append(day.replace("::65", ""))
-		total_absent_on_day = 0
-		total_leave_on_day = 0
-		total_present_on_day = 0
-		total_holiday = 0
-		for emp in emp_att_map.keys():
-			if emp_att_map[emp][idx]:
-				if emp_att_map[emp][idx] == "A":
-					total_absent_on_day += 1
-				if emp_att_map[emp][idx] in ["P", "WFH"]:
-					total_present_on_day += 1
-				if emp_att_map[emp][idx] == "HD":
-					total_present_on_day += 0.5
-					total_leave_on_day += 0.5
-				if emp_att_map[emp][idx] == "L":
-					total_leave_on_day += 1
+def get_message() -> str:
+	message = ""
+	colors = ["green", "red", "orange", "green", "#318AD8", "", ""]
 
-		datasets[0]["values"].append(total_absent_on_day)
-		datasets[1]["values"].append(total_present_on_day)
-		datasets[2]["values"].append(total_leave_on_day)
+	count = 0
+	for status, abbr in status_map.items():
+		message += f"""
+			<span style='border-left: 2px solid {colors[count]}; padding-right: 12px; padding-left: 5px; margin-right: 3px;'>
+				{status} - {abbr}
+			</span>
+		"""
+		count += 1
 
-	chart = {"data": {"labels": labels, "datasets": datasets}}
-
-	chart["type"] = "line"
-
-	return chart
+	return message
 
 
-def add_data(
-	employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_types=None
-):
-
-	record = []
-	emp_att_map = {}
-	for emp in employee_map:
-		emp_det = employee_map.get(emp)
-		if not emp_det or emp not in att_map:
-			continue
-
-		row = []
-		if filters.group_by:
-			row += [" "]
-		row += [emp, emp_det.employee_name]
-
-		total_p = total_a = total_l = total_h = total_um = 0.0
-		emp_status_map = []
-		for day in range(filters["total_days_in_month"]):
-			status = None
-			status = att_map.get(emp).get(day + 1)
-
-			if status is None and holiday_map:
-				emp_holiday_list = emp_det.holiday_list if emp_det.holiday_list else default_holiday_list
-
-				if emp_holiday_list in holiday_map:
-					for idx, ele in enumerate(holiday_map[emp_holiday_list]):
-						if day + 1 == holiday_map[emp_holiday_list][idx][0]:
-							if holiday_map[emp_holiday_list][idx][1]:
-								status = "Weekly Off"
-							else:
-								status = "Holiday"
-							total_h += 1
-
-			abbr = status_map.get(status, "")
-			emp_status_map.append(abbr)
-
-			if filters.summarized_view:
-				if status == "Present" or status == "Work From Home":
-					total_p += 1
-				elif status == "Absent":
-					total_a += 1
-				elif status == "On Leave":
-					total_l += 1
-				elif status == "Half Day":
-					total_p += 0.5
-					total_a += 0.5
-					total_l += 0.5
-				elif not status:
-					total_um += 1
-
-		if not filters.summarized_view:
-			row += emp_status_map
-
-		if filters.summarized_view:
-			row += [total_p, total_l, total_a, total_h, total_um]
-
-		if not filters.get("employee"):
-			filters.update({"employee": emp})
-			conditions += " and employee = %(employee)s"
-		elif not filters.get("employee") == emp:
-			filters.update({"employee": emp})
-
-		if filters.summarized_view:
-			leave_details = frappe.db.sql(
-				"""select leave_type, status, count(*) as count from `tabAttendance`\
-				where leave_type is not NULL %s group by leave_type, status"""
-				% conditions,
-				filters,
-				as_dict=1,
-			)
-
-			time_default_counts = frappe.db.sql(
-				"""select (select count(*) from `tabAttendance` where \
-				late_entry = 1 %s) as late_entry_count, (select count(*) from tabAttendance where \
-				early_exit = 1 %s) as early_exit_count"""
-				% (conditions, conditions),
-				filters,
-			)
-
-			leaves = {}
-			for d in leave_details:
-				if d.status == "Half Day":
-					d.count = d.count * 0.5
-				if d.leave_type in leaves:
-					leaves[d.leave_type] += d.count
-				else:
-					leaves[d.leave_type] = d.count
-
-			for d in leave_types:
-				if d.name in leaves:
-					row.append(leaves[d.name])
-				else:
-					row.append("0.0")
-
-			row.extend([time_default_counts[0][0], time_default_counts[0][1]])
-		emp_att_map[emp] = emp_status_map
-		record.append(row)
-
-	return record, emp_att_map
-
-
-def get_columns(filters):
-
+def get_columns(filters: Filters) -> List[Dict]:
 	columns = []
 
 	if filters.group_by:
-		columns = [_(filters.group_by) + ":Link/Branch:120"]
+		columns.append(
+			{
+				"label": _(filters.group_by),
+				"fieldname": frappe.scrub(filters.group_by),
+				"fieldtype": "Link",
+				"options": "Branch",
+				"width": 120,
+			}
+		)
 
-	columns += [_("Employee") + ":Link/Employee:120", _("Employee Name") + ":Data/:120"]
-	days = []
-	for day in range(filters["total_days_in_month"]):
-		date = str(filters.year) + "-" + str(filters.month) + "-" + str(day + 1)
-		day_name = day_abbr[getdate(date).weekday()]
-		days.append(cstr(day + 1) + " " + day_name + "::65")
-	if not filters.summarized_view:
-		columns += days
-
-	if filters.summarized_view:
-		columns += [
-			_("Total Present") + ":Float:120",
-			_("Total Leaves") + ":Float:120",
-			_("Total Absent") + ":Float:120",
-			_("Total Holidays") + ":Float:120",
-			_("Unmarked Days") + ":Float:120",
+	columns.extend(
+		[
+			{
+				"label": _("Employee"),
+				"fieldname": "employee",
+				"fieldtype": "Link",
+				"options": "Employee",
+				"width": 135,
+			},
+			{"label": _("Employee Name"), "fieldname": "employee_name", "fieldtype": "Data", "width": 120},
 		]
-	return columns, days
-
-
-def get_attendance_list(conditions, filters):
-	attendance_list = frappe.db.sql(
-		"""select employee, day(attendance_date) as day_of_month,
-		status from tabAttendance where docstatus = 1 %s order by employee, attendance_date"""
-		% conditions,
-		filters,
-		as_dict=1,
 	)
 
-	if not attendance_list:
-		msgprint(_("No attendance record found"), alert=True, indicator="orange")
+	if filters.summarized_view:
+		columns.extend(
+			[
+				{
+					"label": _("Total Present"),
+					"fieldname": "total_present",
+					"fieldtype": "Float",
+					"width": 110,
+				},
+				{"label": _("Total Leaves"), "fieldname": "total_leaves", "fieldtype": "Float", "width": 110},
+				{"label": _("Total Absent"), "fieldname": "total_absent", "fieldtype": "Float", "width": 110},
+				{
+					"label": _("Total Holidays"),
+					"fieldname": "total_holidays",
+					"fieldtype": "Float",
+					"width": 120,
+				},
+				{
+					"label": _("Unmarked Days"),
+					"fieldname": "unmarked_days",
+					"fieldtype": "Float",
+					"width": 130,
+				},
+			]
+		)
+		columns.extend(get_columns_for_leave_types())
+		columns.extend(
+			[
+				{
+					"label": _("Total Late Entries"),
+					"fieldname": "total_late_entries",
+					"fieldtype": "Float",
+					"width": 140,
+				},
+				{
+					"label": _("Total Early Exits"),
+					"fieldname": "total_early_exits",
+					"fieldtype": "Float",
+					"width": 140,
+				},
+			]
+		)
+	else:
+		columns.append({"label": _("Shift"), "fieldname": "shift", "fieldtype": "Data", "width": 120})
+		columns.extend(get_columns_for_days(filters))
 
-	att_map = {}
+	return columns
+
+
+def get_columns_for_leave_types() -> List[Dict]:
+	leave_types = frappe.db.get_all("Leave Type", pluck="name")
+	types = []
+	for entry in leave_types:
+		types.append(
+			{"label": entry, "fieldname": frappe.scrub(entry), "fieldtype": "Float", "width": 120}
+		)
+
+	return types
+
+
+def get_columns_for_days(filters: Filters) -> List[Dict]:
+	total_days = get_total_days_in_month(filters)
+	days = []
+
+	for day in range(1, total_days + 1):
+		# forms the dates from selected year and month from filters
+		date = "{}-{}-{}".format(cstr(filters.year), cstr(filters.month), cstr(day))
+		# gets abbr from weekday number
+		weekday = day_abbr[getdate(date).weekday()]
+		# sets days as 1 Mon, 2 Tue, 3 Wed
+		label = "{} {}".format(cstr(day), weekday)
+		days.append({"label": label, "fieldtype": "Data", "fieldname": day, "width": 65})
+
+	return days
+
+
+def get_total_days_in_month(filters: Filters) -> int:
+	return monthrange(cint(filters.year), cint(filters.month))[1]
+
+
+def get_data(filters: Filters, attendance_map: Dict) -> List[Dict]:
+	employee_details, group_by_param_values = get_employee_related_details(
+		filters.group_by, filters.company
+	)
+	holiday_map = get_holiday_map(filters)
+	data = []
+
+	if filters.group_by:
+		group_by_column = frappe.scrub(filters.group_by)
+
+		for value in group_by_param_values:
+			if not value:
+				continue
+
+			records = get_rows(employee_details[value], filters, holiday_map, attendance_map)
+
+			if records:
+				data.append({group_by_column: frappe.bold(value)})
+				data.extend(records)
+	else:
+		data = get_rows(employee_details, filters, holiday_map, attendance_map)
+
+	return data
+
+
+def get_attendance_map(filters: Filters) -> Dict:
+	"""Returns a dictionary of employee wise attendance map as per shifts for all the days of the month like
+	{
+	        'employee1': {
+	                'Morning Shift': {1: 'Present', 2: 'Absent', ...}
+	                'Evening Shift': {1: 'Absent', 2: 'Present', ...}
+	        },
+	        'employee2': {
+	                'Afternoon Shift': {1: 'Present', 2: 'Absent', ...}
+	                'Night Shift': {1: 'Absent', 2: 'Absent', ...}
+	        }
+	}
+	"""
+	Attendance = frappe.qb.DocType("Attendance")
+	query = (
+		frappe.qb.from_(Attendance)
+		.select(
+			Attendance.employee,
+			Extract("day", Attendance.attendance_date).as_("day_of_month"),
+			Attendance.status,
+			Attendance.shift,
+		)
+		.where(
+			(Attendance.docstatus == 1)
+			& (Attendance.company == filters.company)
+			& (Extract("month", Attendance.attendance_date) == filters.month)
+			& (Extract("year", Attendance.attendance_date) == filters.year)
+		)
+	)
+	if filters.employee:
+		query = query.where(Attendance.employee == filters.employee)
+	query = query.orderby(Attendance.employee, Attendance.attendance_date)
+
+	attendance_list = query.run(as_dict=1)
+	attendance_map = {}
+
 	for d in attendance_list:
-		att_map.setdefault(d.employee, frappe._dict()).setdefault(d.day_of_month, "")
-		att_map[d.employee][d.day_of_month] = d.status
+		attendance_map.setdefault(d.employee, frappe._dict()).setdefault(d.shift, frappe._dict())
+		attendance_map[d.employee][d.shift][d.day_of_month] = d.status
 
-	return att_map
+	return attendance_map
 
 
-def get_conditions(filters):
-	if not (filters.get("month") and filters.get("year")):
-		msgprint(_("Please select month and year"), raise_exception=1)
-
-	filters["total_days_in_month"] = monthrange(cint(filters.year), cint(filters.month))[1]
-
-	conditions = " and month(attendance_date) = %(month)s and year(attendance_date) = %(year)s"
-
-	if filters.get("company"):
-		conditions += " and company = %(company)s"
-	if filters.get("employee"):
-		conditions += " and employee = %(employee)s"
-
-	return conditions, filters
-
-
-def get_employee_details(group_by, company):
-	emp_map = {}
-	query = """select name, employee_name, designation, department, branch, company,
-		holiday_list from `tabEmployee` where company = %s """ % frappe.db.escape(
-		company
+def get_employee_related_details(group_by: str, company: str) -> Tuple[Dict, List]:
+	"""Returns
+	1. nested dict for employee details
+	2. list of values for the group by filter
+	"""
+	Employee = frappe.qb.DocType("Employee")
+	query = (
+		frappe.qb.from_(Employee)
+		.select(
+			Employee.name,
+			Employee.employee_name,
+			Employee.designation,
+			Employee.grade,
+			Employee.department,
+			Employee.branch,
+			Employee.company,
+			Employee.holiday_list,
+		)
+		.where(Employee.company == company)
 	)
 
 	if group_by:
 		group_by = group_by.lower()
-		query += " order by " + group_by + " ASC"
+		query = query.orderby(group_by)
 
-	employee_details = frappe.db.sql(query, as_dict=1)
+	employee_details = query.run(as_dict=True)
 
-	group_by_parameters = []
+	group_by_param_values = []
+	emp_map = {}
+
 	if group_by:
+		for parameter, employees in groupby(employee_details, key=lambda d: d[group_by]):
+			group_by_param_values.append(parameter)
+			emp_map.setdefault(parameter, frappe._dict())
 
-		group_by_parameters = list(
-			set(detail.get(group_by, "") for detail in employee_details if detail.get(group_by, ""))
-		)
-		for parameter in group_by_parameters:
-			emp_map[parameter] = {}
-
-	for d in employee_details:
-		if group_by and len(group_by_parameters):
-			if d.get(group_by, None):
-
-				emp_map[d.get(group_by)][d.name] = d
-		else:
-			emp_map[d.name] = d
-
-	if not group_by:
-		return emp_map
+			for emp in employees:
+				emp_map[parameter][emp.name] = emp
 	else:
-		return emp_map, group_by_parameters
+		for emp in employee_details:
+			emp_map[emp.name] = emp
+
+	return emp_map, group_by_param_values
 
 
-def get_holiday(holiday_list, month):
+def get_holiday_map(filters: Filters) -> Dict[str, List[Dict]]:
+	"""
+	Returns a dict of holidays falling in the filter month and year
+	with list name as key and list of holidays as values like
+	{
+	        'Holiday List 1': [
+	                {'day_of_month': '0' , 'weekly_off': 1},
+	                {'day_of_month': '1', 'weekly_off': 0}
+	        ],
+	        'Holiday List 2': [
+	                {'day_of_month': '0' , 'weekly_off': 1},
+	                {'day_of_month': '1', 'weekly_off': 0}
+	        ]
+	}
+	"""
+	# add default holiday list too
+	holiday_lists = frappe.db.get_all("Holiday List", pluck="name")
+	default_holiday_list = frappe.get_cached_value("Company", filters.company, "default_holiday_list")
+	holiday_lists.append(default_holiday_list)
+
 	holiday_map = frappe._dict()
-	for d in holiday_list:
-		if d:
-			holiday_map.setdefault(
-				d,
-				frappe.db.sql(
-					"""select day(holiday_date), weekly_off from `tabHoliday`
-				where parent=%s and month(holiday_date)=%s""",
-					(d, month),
-				),
+	Holiday = frappe.qb.DocType("Holiday")
+
+	for d in holiday_lists:
+		if not d:
+			continue
+
+		holidays = (
+			frappe.qb.from_(Holiday)
+			.select(Extract("day", Holiday.holiday_date).as_("day_of_month"), Holiday.weekly_off)
+			.where(
+				(Holiday.parent == d)
+				& (Extract("month", Holiday.holiday_date) == filters.month)
+				& (Extract("year", Holiday.holiday_date) == filters.year)
 			)
+		).run(as_dict=True)
+
+		holiday_map.setdefault(d, holidays)
 
 	return holiday_map
 
 
-@frappe.whitelist()
-def get_attendance_years():
-	year_list = frappe.db.sql_list(
-		"""select distinct YEAR(attendance_date) from tabAttendance ORDER BY YEAR(attendance_date) DESC"""
+def get_rows(
+	employee_details: Dict, filters: Filters, holiday_map: Dict, attendance_map: Dict
+) -> List[Dict]:
+	records = []
+	default_holiday_list = frappe.get_cached_value("Company", filters.company, "default_holiday_list")
+
+	for employee, details in employee_details.items():
+		emp_holiday_list = details.holiday_list or default_holiday_list
+		holidays = holiday_map.get(emp_holiday_list)
+
+		if filters.summarized_view:
+			attendance = get_attendance_status_for_summarized_view(employee, filters, holidays)
+			if not attendance:
+				continue
+
+			leave_summary = get_leave_summary(employee, filters)
+			entry_exits_summary = get_entry_exits_summary(employee, filters)
+
+			row = {"employee": employee, "employee_name": details.employee_name}
+			set_defaults_for_summarized_view(filters, row)
+			row.update(attendance)
+			row.update(leave_summary)
+			row.update(entry_exits_summary)
+
+			records.append(row)
+		else:
+			employee_attendance = attendance_map.get(employee)
+			if not employee_attendance:
+				continue
+
+			attendance_for_employee = get_attendance_status_for_detailed_view(
+				employee, filters, employee_attendance, holidays
+			)
+			# set employee details in the first row
+			attendance_for_employee[0].update(
+				{"employee": employee, "employee_name": details.employee_name}
+			)
+
+			records.extend(attendance_for_employee)
+
+	return records
+
+
+def set_defaults_for_summarized_view(filters, row):
+	for entry in get_columns(filters):
+		if entry.get("fieldtype") == "Float":
+			row[entry.get("fieldname")] = 0.0
+
+
+def get_attendance_status_for_summarized_view(
+	employee: str, filters: Filters, holidays: List
+) -> Dict:
+	"""Returns dict of attendance status for employee like
+	{'total_present': 1.5, 'total_leaves': 0.5, 'total_absent': 13.5, 'total_holidays': 8, 'unmarked_days': 5}
+	"""
+	summary, attendance_days = get_attendance_summary_and_days(employee, filters)
+	if not any(summary.values()):
+		return {}
+
+	total_days = get_total_days_in_month(filters)
+	total_holidays = total_unmarked_days = 0
+
+	for day in range(1, total_days + 1):
+		if day in attendance_days:
+			continue
+
+		status = get_holiday_status(day, holidays)
+		if status in ["Weekly Off", "Holiday"]:
+			total_holidays += 1
+		elif not status:
+			total_unmarked_days += 1
+
+	return {
+		"total_present": summary.total_present + summary.total_half_days,
+		"total_leaves": summary.total_leaves + summary.total_half_days,
+		"total_absent": summary.total_absent + summary.total_half_days,
+		"total_holidays": total_holidays,
+		"unmarked_days": total_unmarked_days,
+	}
+
+
+def get_attendance_summary_and_days(employee: str, filters: Filters) -> Tuple[Dict, List]:
+	Attendance = frappe.qb.DocType("Attendance")
+
+	present_case = (
+		frappe.qb.terms.Case()
+		.when(((Attendance.status == "Present") | (Attendance.status == "Work From Home")), 1)
+		.else_(0)
 	)
-	if not year_list:
+	sum_present = Sum(present_case).as_("total_present")
+
+	absent_case = frappe.qb.terms.Case().when(Attendance.status == "Absent", 1).else_(0)
+	sum_absent = Sum(absent_case).as_("total_absent")
+
+	leave_case = frappe.qb.terms.Case().when(Attendance.status == "On Leave", 1).else_(0)
+	sum_leave = Sum(leave_case).as_("total_leaves")
+
+	half_day_case = frappe.qb.terms.Case().when(Attendance.status == "Half Day", 0.5).else_(0)
+	sum_half_day = Sum(half_day_case).as_("total_half_days")
+
+	summary = (
+		frappe.qb.from_(Attendance)
+		.select(
+			sum_present,
+			sum_absent,
+			sum_leave,
+			sum_half_day,
+		)
+		.where(
+			(Attendance.docstatus == 1)
+			& (Attendance.employee == employee)
+			& (Attendance.company == filters.company)
+			& (Extract("month", Attendance.attendance_date) == filters.month)
+			& (Extract("year", Attendance.attendance_date) == filters.year)
+		)
+	).run(as_dict=True)
+
+	days = (
+		frappe.qb.from_(Attendance)
+		.select(Extract("day", Attendance.attendance_date).as_("day_of_month"))
+		.distinct()
+		.where(
+			(Attendance.docstatus == 1)
+			& (Attendance.employee == employee)
+			& (Attendance.company == filters.company)
+			& (Extract("month", Attendance.attendance_date) == filters.month)
+			& (Extract("year", Attendance.attendance_date) == filters.year)
+		)
+	).run(pluck=True)
+
+	return summary[0], days
+
+
+def get_attendance_status_for_detailed_view(
+	employee: str, filters: Filters, employee_attendance: Dict, holidays: List
+) -> List[Dict]:
+	"""Returns list of shift-wise attendance status for employee
+	[
+	        {'shift': 'Morning Shift', 1: 'A', 2: 'P', 3: 'A'....},
+	        {'shift': 'Evening Shift', 1: 'P', 2: 'A', 3: 'P'....}
+	]
+	"""
+	total_days = get_total_days_in_month(filters)
+	attendance_values = []
+
+	for shift, status_dict in employee_attendance.items():
+		row = {"shift": shift}
+
+		for day in range(1, total_days + 1):
+			status = status_dict.get(day)
+			if status is None and holidays:
+				status = get_holiday_status(day, holidays)
+
+			abbr = status_map.get(status, "")
+			row[day] = abbr
+
+		attendance_values.append(row)
+
+	return attendance_values
+
+
+def get_holiday_status(day: int, holidays: List) -> str:
+	status = None
+	for holiday in holidays:
+		if day == holiday.get("day_of_month"):
+			if holiday.get("weekly_off"):
+				status = "Weekly Off"
+			else:
+				status = "Holiday"
+			break
+	return status
+
+
+def get_leave_summary(employee: str, filters: Filters) -> Dict[str, float]:
+	"""Returns a dict of leave type and corresponding leaves taken by employee like:
+	{'leave_without_pay': 1.0, 'sick_leave': 2.0}
+	"""
+	Attendance = frappe.qb.DocType("Attendance")
+	day_case = frappe.qb.terms.Case().when(Attendance.status == "Half Day", 0.5).else_(1)
+	sum_leave_days = Sum(day_case).as_("leave_days")
+
+	leave_details = (
+		frappe.qb.from_(Attendance)
+		.select(Attendance.leave_type, sum_leave_days)
+		.where(
+			(Attendance.employee == employee)
+			& (Attendance.docstatus == 1)
+			& (Attendance.company == filters.company)
+			& ((Attendance.leave_type.isnotnull()) | (Attendance.leave_type != ""))
+			& (Extract("month", Attendance.attendance_date) == filters.month)
+			& (Extract("year", Attendance.attendance_date) == filters.year)
+		)
+		.groupby(Attendance.leave_type)
+	).run(as_dict=True)
+
+	leaves = {}
+	for d in leave_details:
+		leave_type = frappe.scrub(d.leave_type)
+		leaves[leave_type] = d.leave_days
+
+	return leaves
+
+
+def get_entry_exits_summary(employee: str, filters: Filters) -> Dict[str, float]:
+	"""Returns total late entries and total early exits for employee like:
+	{'total_late_entries': 5, 'total_early_exits': 2}
+	"""
+	Attendance = frappe.qb.DocType("Attendance")
+
+	late_entry_case = frappe.qb.terms.Case().when(Attendance.late_entry == "1", "1")
+	count_late_entries = Count(late_entry_case).as_("total_late_entries")
+
+	early_exit_case = frappe.qb.terms.Case().when(Attendance.early_exit == "1", "1")
+	count_early_exits = Count(early_exit_case).as_("total_early_exits")
+
+	entry_exits = (
+		frappe.qb.from_(Attendance)
+		.select(count_late_entries, count_early_exits)
+		.where(
+			(Attendance.docstatus == 1)
+			& (Attendance.employee == employee)
+			& (Attendance.company == filters.company)
+			& (Extract("month", Attendance.attendance_date) == filters.month)
+			& (Extract("year", Attendance.attendance_date) == filters.year)
+		)
+	).run(as_dict=True)
+
+	return entry_exits[0]
+
+
+@frappe.whitelist()
+def get_attendance_years() -> str:
+	"""Returns all the years for which attendance records exist"""
+	Attendance = frappe.qb.DocType("Attendance")
+	year_list = (
+		frappe.qb.from_(Attendance)
+		.select(Extract("year", Attendance.attendance_date).as_("year"))
+		.distinct()
+	).run(as_dict=True)
+
+	if year_list:
+		year_list.sort(key=lambda d: d.year, reverse=True)
+	else:
 		year_list = [getdate().year]
 
-	return "\n".join(str(year) for year in year_list)
+	return "\n".join(cstr(entry.year) for entry in year_list)
+
+
+def get_chart_data(attendance_map: Dict, filters: Filters) -> Dict:
+	days = get_columns_for_days(filters)
+	labels = []
+	absent = []
+	present = []
+	leave = []
+
+	for day in days:
+		labels.append(day["label"])
+		total_absent_on_day = total_leaves_on_day = total_present_on_day = 0
+
+		for employee, attendance_dict in attendance_map.items():
+			for shift, attendance in attendance_dict.items():
+				attendance_on_day = attendance.get(day["fieldname"])
+
+				if attendance_on_day == "Absent":
+					total_absent_on_day += 1
+				elif attendance_on_day in ["Present", "Work From Home"]:
+					total_present_on_day += 1
+				elif attendance_on_day == "Half Day":
+					total_present_on_day += 0.5
+					total_leaves_on_day += 0.5
+				elif attendance_on_day == "On Leave":
+					total_leaves_on_day += 1
+
+		absent.append(total_absent_on_day)
+		present.append(total_present_on_day)
+		leave.append(total_leaves_on_day)
+
+	return {
+		"data": {
+			"labels": labels,
+			"datasets": [
+				{"name": "Absent", "values": absent},
+				{"name": "Present", "values": present},
+				{"name": "Leave", "values": leave},
+			],
+		},
+		"type": "line",
+		"colors": ["red", "green", "blue"],
+	}
diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
index 91da08e..cde7dd3 100644
--- a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
+++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py
@@ -1,18 +1,32 @@
 import frappe
 from dateutil.relativedelta import relativedelta
 from frappe.tests.utils import FrappeTestCase
-from frappe.utils import now_datetime
+from frappe.utils import get_year_ending, get_year_start, getdate, now_datetime
 
 from erpnext.hr.doctype.attendance.attendance import mark_attendance
 from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
+from erpnext.hr.doctype.leave_application.test_leave_application import make_allocation_record
 from erpnext.hr.report.monthly_attendance_sheet.monthly_attendance_sheet import execute
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+	make_holiday_list,
+	make_leave_application,
+)
+
+test_dependencies = ["Shift Type"]
 
 
 class TestMonthlyAttendanceSheet(FrappeTestCase):
 	def setUp(self):
-		self.employee = make_employee("test_employee@example.com")
-		frappe.db.delete("Attendance", {"employee": self.employee})
+		self.employee = make_employee("test_employee@example.com", company="_Test Company")
+		frappe.db.delete("Attendance")
 
+		date = getdate()
+		from_date = get_year_start(date)
+		to_date = get_year_ending(date)
+		make_holiday_list(from_date=from_date, to_date=to_date)
+
+	@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
 	def test_monthly_attendance_sheet_report(self):
 		now = now_datetime()
 		previous_month = now.month - 1
@@ -33,14 +47,203 @@
 			}
 		)
 		report = execute(filters=filters)
-		employees = report[1][0]
+
+		record = report[1][0]
 		datasets = report[3]["data"]["datasets"]
 		absent = datasets[0]["values"]
 		present = datasets[1]["values"]
 		leaves = datasets[2]["values"]
 
-		# ensure correct attendance is reflect on the report
-		self.assertIn(self.employee, employees)
+		# ensure correct attendance is reflected on the report
+		self.assertEqual(self.employee, record.get("employee"))
 		self.assertEqual(absent[0], 1)
 		self.assertEqual(present[1], 1)
 		self.assertEqual(leaves[2], 1)
+
+	@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+	def test_monthly_attendance_sheet_with_detailed_view(self):
+		now = now_datetime()
+		previous_month = now.month - 1
+		previous_month_first = now.replace(day=1).replace(month=previous_month).date()
+
+		company = frappe.db.get_value("Employee", self.employee, "company")
+
+		# attendance with shift
+		mark_attendance(self.employee, previous_month_first, "Absent", "Day Shift")
+		mark_attendance(
+			self.employee, previous_month_first + relativedelta(days=1), "Present", "Day Shift"
+		)
+
+		# attendance without shift
+		mark_attendance(self.employee, previous_month_first + relativedelta(days=2), "On Leave")
+		mark_attendance(self.employee, previous_month_first + relativedelta(days=3), "Present")
+
+		filters = frappe._dict(
+			{
+				"month": previous_month,
+				"year": now.year,
+				"company": company,
+			}
+		)
+		report = execute(filters=filters)
+
+		day_shift_row = report[1][0]
+		row_without_shift = report[1][1]
+
+		self.assertEqual(day_shift_row["shift"], "Day Shift")
+		self.assertEqual(day_shift_row[1], "A")  # absent on the 1st day of the month
+		self.assertEqual(day_shift_row[2], "P")  # present on the 2nd day
+
+		self.assertEqual(row_without_shift["shift"], None)
+		self.assertEqual(row_without_shift[3], "L")  # on leave on the 3rd day
+		self.assertEqual(row_without_shift[4], "P")  # present on the 4th day
+
+	@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+	def test_monthly_attendance_sheet_with_summarized_view(self):
+		now = now_datetime()
+		previous_month = now.month - 1
+		previous_month_first = now.replace(day=1).replace(month=previous_month).date()
+
+		company = frappe.db.get_value("Employee", self.employee, "company")
+
+		# attendance with shift
+		mark_attendance(self.employee, previous_month_first, "Absent", "Day Shift")
+		mark_attendance(
+			self.employee, previous_month_first + relativedelta(days=1), "Present", "Day Shift"
+		)
+		mark_attendance(
+			self.employee, previous_month_first + relativedelta(days=2), "Half Day"
+		)  # half day
+
+		mark_attendance(
+			self.employee, previous_month_first + relativedelta(days=3), "Present"
+		)  # attendance without shift
+		mark_attendance(
+			self.employee, previous_month_first + relativedelta(days=4), "Present", late_entry=1
+		)  # late entry
+		mark_attendance(
+			self.employee, previous_month_first + relativedelta(days=5), "Present", early_exit=1
+		)  # early exit
+
+		leave_application = get_leave_application(self.employee)
+
+		filters = frappe._dict(
+			{"month": previous_month, "year": now.year, "company": company, "summarized_view": 1}
+		)
+		report = execute(filters=filters)
+
+		row = report[1][0]
+		self.assertEqual(row["employee"], self.employee)
+
+		# 4 present + half day absent 0.5
+		self.assertEqual(row["total_present"], 4.5)
+		# 1 present + half day absent 0.5
+		self.assertEqual(row["total_absent"], 1.5)
+		# leave days + half day leave 0.5
+		self.assertEqual(row["total_leaves"], leave_application.total_leave_days + 0.5)
+
+		self.assertEqual(row["_test_leave_type"], leave_application.total_leave_days)
+		self.assertEqual(row["total_late_entries"], 1)
+		self.assertEqual(row["total_early_exits"], 1)
+
+	@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+	def test_attendance_with_group_by_filter(self):
+		now = now_datetime()
+		previous_month = now.month - 1
+		previous_month_first = now.replace(day=1).replace(month=previous_month).date()
+
+		company = frappe.db.get_value("Employee", self.employee, "company")
+
+		# attendance with shift
+		mark_attendance(self.employee, previous_month_first, "Absent", "Day Shift")
+		mark_attendance(
+			self.employee, previous_month_first + relativedelta(days=1), "Present", "Day Shift"
+		)
+
+		# attendance without shift
+		mark_attendance(self.employee, previous_month_first + relativedelta(days=2), "On Leave")
+		mark_attendance(self.employee, previous_month_first + relativedelta(days=3), "Present")
+
+		filters = frappe._dict(
+			{"month": previous_month, "year": now.year, "company": company, "group_by": "Department"}
+		)
+		report = execute(filters=filters)
+
+		department = frappe.db.get_value("Employee", self.employee, "department")
+		department_row = report[1][0]
+		self.assertIn(department, department_row["department"])
+
+		day_shift_row = report[1][1]
+		row_without_shift = report[1][2]
+
+		self.assertEqual(day_shift_row["shift"], "Day Shift")
+		self.assertEqual(day_shift_row[1], "A")  # absent on the 1st day of the month
+		self.assertEqual(day_shift_row[2], "P")  # present on the 2nd day
+
+		self.assertEqual(row_without_shift["shift"], None)
+		self.assertEqual(row_without_shift[3], "L")  # on leave on the 3rd day
+		self.assertEqual(row_without_shift[4], "P")  # present on the 4th day
+
+	def test_attendance_with_employee_filter(self):
+		now = now_datetime()
+		previous_month = now.month - 1
+		previous_month_first = now.replace(day=1).replace(month=previous_month).date()
+
+		company = frappe.db.get_value("Employee", self.employee, "company")
+
+		# mark different attendance status on first 3 days of previous month
+		mark_attendance(self.employee, previous_month_first, "Absent")
+		mark_attendance(self.employee, previous_month_first + relativedelta(days=1), "Present")
+		mark_attendance(self.employee, previous_month_first + relativedelta(days=2), "On Leave")
+
+		filters = frappe._dict(
+			{"month": previous_month, "year": now.year, "company": company, "employee": self.employee}
+		)
+		report = execute(filters=filters)
+
+		record = report[1][0]
+		datasets = report[3]["data"]["datasets"]
+		absent = datasets[0]["values"]
+		present = datasets[1]["values"]
+		leaves = datasets[2]["values"]
+
+		# ensure correct attendance is reflected on the report
+		self.assertEqual(self.employee, record.get("employee"))
+		self.assertEqual(absent[0], 1)
+		self.assertEqual(present[1], 1)
+		self.assertEqual(leaves[2], 1)
+
+	@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+	def test_validations(self):
+		# validation error for filters without month and year
+		self.assertRaises(frappe.ValidationError, execute_report_with_invalid_filters)
+
+		# execute report without attendance record
+		now = now_datetime()
+		previous_month = now.month - 1
+
+		company = frappe.db.get_value("Employee", self.employee, "company")
+		filters = frappe._dict(
+			{"month": previous_month, "year": now.year, "company": company, "group_by": "Department"}
+		)
+		report = execute(filters=filters)
+		self.assertEqual(report, ([], [], None, None))
+
+
+def get_leave_application(employee):
+	now = now_datetime()
+	previous_month = now.month - 1
+
+	date = getdate()
+	year_start = getdate(get_year_start(date))
+	year_end = getdate(get_year_ending(date))
+	make_allocation_record(employee=employee, from_date=year_start, to_date=year_end)
+
+	from_date = now.replace(day=7).replace(month=previous_month).date()
+	to_date = now.replace(day=8).replace(month=previous_month).date()
+	return make_leave_application(employee, from_date, to_date, "_Test Leave Type")
+
+
+def execute_report_with_invalid_filters():
+	filters = frappe._dict({"company": "_Test Company", "group_by": "Department"})
+	execute(filters=filters)
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index fd69a9b..40ab805 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -353,6 +353,17 @@
 			allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
 			create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
 
+			if e_leave_type.based_on_date_of_joining:
+				text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format(
+					frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
+				)
+			else:
+				text = _("allocated {0} leave(s) via scheduler on {1}").format(
+					frappe.bold(earned_leaves), frappe.bold(formatdate(today_date))
+				)
+
+			allocation.add_comment(comment_type="Info", text=text)
+
 
 def get_monthly_earned_leave(annual_leaves, frequency, rounding):
 	earned_leaves = 0.0
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 8cffe88..2535180 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -387,13 +387,13 @@
 		gle_map = []
 
 		if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
-			remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(
+			remarks = _("Shortfall Repayment of {0}.<br>Repayment against Loan: {1}").format(
 				self.shortfall_amount, self.against_loan
 			)
 		elif self.shortfall_amount:
 			remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount)
 		else:
-			remarks = _("Repayment against Loan: ") + self.against_loan
+			remarks = _("Repayment against Loan:") + " " + self.against_loan
 
 		if self.repay_from_salary:
 			payment_account = self.payroll_payable_account
@@ -584,9 +584,10 @@
 			balance_amount / len(loan_doc.get("repayment_schedule")) - accrued_entries
 		)
 	else:
-		if not cancel:
+		repayment_period = loan_doc.repayment_periods - accrued_entries
+		if not cancel and repayment_period > 0:
 			monthly_repayment_amount = get_monthly_repayment_amount(
-				balance_amount, loan_doc.rate_of_interest, loan_doc.repayment_periods - accrued_entries
+				balance_amount, loan_doc.rate_of_interest, repayment_period
 			)
 		else:
 			monthly_repayment_amount = last_repayment_amount
@@ -745,6 +746,8 @@
 	if payment_type == "Loan Closure":
 		amounts["payable_principal_amount"] = amounts["pending_principal_amount"]
 		amounts["interest_amount"] += amounts["unaccrued_interest"]
-		amounts["payable_amount"] = amounts["payable_principal_amount"] + amounts["interest_amount"]
+		amounts["payable_amount"] = (
+			amounts["payable_principal_amount"] + amounts["interest_amount"] + amounts["penalty_amount"]
+		)
 
 	return amounts
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index 256f660..9a23c07 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -250,7 +250,7 @@
 					_("Serial No {0} does not belong to Item {1}").format(
 						frappe.bold(serial_no), frappe.bold(item_code)
 					),
-					title="Invalid",
+					title=_("Invalid"),
 				)
 
 			if sr_details.warranty_expiry_date and getdate(sr_details.warranty_expiry_date) >= getdate(
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
index 29a1784..66f4426 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
@@ -20,7 +20,7 @@
 
 	def validate_purpose_table(self):
 		if not self.purposes:
-			frappe.throw(_("Add Items in the Purpose Table"), title="Purposes Required")
+			frappe.throw(_("Add Items in the Purpose Table"), title=_("Purposes Required"))
 
 	def validate_maintenance_date(self):
 		if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail:
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 9b6cf46..fefb2e5 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -697,15 +697,6 @@
 		self.scrap_material_cost = total_sm_cost
 		self.base_scrap_material_cost = base_total_sm_cost
 
-	def update_new_bom(self, old_bom, new_bom, rate):
-		for d in self.get("items"):
-			if d.bom_no != old_bom:
-				continue
-
-			d.bom_no = new_bom
-			d.rate = rate
-			d.amount = (d.stock_qty or d.qty) * rate
-
 	def update_exploded_items(self, save=True):
 		"""Update Flat BOM, following will be correct data"""
 		self.get_exploded_items()
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 524f45b..62fc072 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -251,7 +251,7 @@
 		self.assertEqual(bom.items[2].rate, 0)
 		# test in Purchase Order sourced_by_supplier is not added to Supplied Item
 		po = create_purchase_order(
-			item_code=item_code, qty=1, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			item_code=item_code, qty=1, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 		bom_items = sorted([d.item_code for d in bom.items if d.sourced_by_supplier != 1])
 		supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index 341f969..b965a43 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -109,7 +109,6 @@
    "read_only": 1
   },
   {
-   "default": "5",
    "depends_on": "eval:parent.doctype == 'BOM'",
    "fieldname": "base_operating_cost",
    "fieldtype": "Currency",
@@ -187,7 +186,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-03-10 06:19:08.462027",
+ "modified": "2022-04-08 01:18:33.547481",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "BOM Operation",
diff --git a/erpnext/manufacturing/doctype/bom_update_log/__init__.py b/erpnext/manufacturing/doctype/bom_update_log/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/__init__.py
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js
new file mode 100644
index 0000000..6da808e
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('BOM Update Log', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
new file mode 100644
index 0000000..98c1acb
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
@@ -0,0 +1,109 @@
+{
+ "actions": [],
+ "autoname": "BOM-UPDT-LOG-.#####",
+ "creation": "2022-03-16 14:23:35.210155",
+ "description": "BOM Update Tool Log with job status maintained",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "current_bom",
+  "new_bom",
+  "column_break_3",
+  "update_type",
+  "status",
+  "error_log",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "current_bom",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Current BOM",
+   "options": "BOM"
+  },
+  {
+   "fieldname": "new_bom",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "New BOM",
+   "options": "BOM"
+  },
+  {
+   "fieldname": "column_break_3",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "update_type",
+   "fieldtype": "Select",
+   "in_list_view": 1,
+   "label": "Update Type",
+   "options": "Replace BOM\nUpdate Cost"
+  },
+  {
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "label": "Status",
+   "options": "Queued\nIn Progress\nCompleted\nFailed"
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "BOM Update Log",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "error_log",
+   "fieldtype": "Link",
+   "label": "Error Log",
+   "options": "Error Log"
+  }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-03-31 12:51:44.885102",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "BOM Update Log",
+ "naming_rule": "Expression (old style)",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "create": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Manufacturing Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
new file mode 100644
index 0000000..139dcbc
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
@@ -0,0 +1,164 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+from typing import Dict, List, Literal, Optional
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import cstr, flt
+
+from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
+
+
+class BOMMissingError(frappe.ValidationError):
+	pass
+
+
+class BOMUpdateLog(Document):
+	def validate(self):
+		if self.update_type == "Replace BOM":
+			self.validate_boms_are_specified()
+			self.validate_same_bom()
+			self.validate_bom_items()
+
+		self.status = "Queued"
+
+	def validate_boms_are_specified(self):
+		if self.update_type == "Replace BOM" and not (self.current_bom and self.new_bom):
+			frappe.throw(
+				msg=_("Please mention the Current and New BOM for replacement."),
+				title=_("Mandatory"),
+				exc=BOMMissingError,
+			)
+
+	def validate_same_bom(self):
+		if cstr(self.current_bom) == cstr(self.new_bom):
+			frappe.throw(_("Current BOM and New BOM can not be same"))
+
+	def validate_bom_items(self):
+		current_bom_item = frappe.db.get_value("BOM", self.current_bom, "item")
+		new_bom_item = frappe.db.get_value("BOM", self.new_bom, "item")
+
+		if current_bom_item != new_bom_item:
+			frappe.throw(_("The selected BOMs are not for the same item"))
+
+	def on_submit(self):
+		if frappe.flags.in_test:
+			return
+
+		if self.update_type == "Replace BOM":
+			boms = {"current_bom": self.current_bom, "new_bom": self.new_bom}
+			frappe.enqueue(
+				method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
+				doc=self,
+				boms=boms,
+				timeout=40000,
+			)
+		else:
+			frappe.enqueue(
+				method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
+				doc=self,
+				update_type="Update Cost",
+				timeout=40000,
+			)
+
+
+def replace_bom(boms: Dict) -> None:
+	"""Replace current BOM with new BOM in parent BOMs."""
+	current_bom = boms.get("current_bom")
+	new_bom = boms.get("new_bom")
+
+	unit_cost = get_new_bom_unit_cost(new_bom)
+	update_new_bom_in_bom_items(unit_cost, current_bom, new_bom)
+
+	frappe.cache().delete_key("bom_children")
+	parent_boms = get_parent_boms(new_bom)
+
+	for bom in parent_boms:
+		bom_obj = frappe.get_doc("BOM", bom)
+		# this is only used for versioning and we do not want
+		# to make separate db calls by using load_doc_before_save
+		# which proves to be expensive while doing bulk replace
+		bom_obj._doc_before_save = bom_obj
+		bom_obj.update_exploded_items()
+		bom_obj.calculate_cost()
+		bom_obj.update_parent_cost()
+		bom_obj.db_update()
+		if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version:
+			bom_obj.save_version()
+
+
+def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:
+	bom_item = frappe.qb.DocType("BOM Item")
+	(
+		frappe.qb.update(bom_item)
+		.set(bom_item.bom_no, new_bom)
+		.set(bom_item.rate, unit_cost)
+		.set(bom_item.amount, (bom_item.stock_qty * unit_cost))
+		.where(
+			(bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")
+		)
+	).run()
+
+
+def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
+	bom_list = bom_list or []
+	bom_item = frappe.qb.DocType("BOM Item")
+
+	parents = (
+		frappe.qb.from_(bom_item)
+		.select(bom_item.parent)
+		.where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM"))
+		.run(as_dict=True)
+	)
+
+	for d in parents:
+		if new_bom == d.parent:
+			frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
+
+		bom_list.append(d.parent)
+		get_parent_boms(d.parent, bom_list)
+
+	return list(set(bom_list))
+
+
+def get_new_bom_unit_cost(new_bom: str) -> float:
+	bom = frappe.qb.DocType("BOM")
+	new_bom_unitcost = (
+		frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == new_bom).run()
+	)
+
+	return flt(new_bom_unitcost[0][0])
+
+
+def run_bom_job(
+	doc: "BOMUpdateLog",
+	boms: Optional[Dict[str, str]] = None,
+	update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
+) -> None:
+	try:
+		doc.db_set("status", "In Progress")
+		if not frappe.flags.in_test:
+			frappe.db.commit()
+
+		frappe.db.auto_commit_on_many_writes = 1
+
+		boms = frappe._dict(boms or {})
+
+		if update_type == "Replace BOM":
+			replace_bom(boms)
+		else:
+			update_cost()
+
+		doc.db_set("status", "Completed")
+
+	except Exception:
+		frappe.db.rollback()
+		error_log = frappe.log_error(message=frappe.get_traceback(), title=_("BOM Update Tool Error"))
+
+		doc.db_set("status", "Failed")
+		doc.db_set("error_log", error_log.name)
+
+	finally:
+		frappe.db.auto_commit_on_many_writes = 0
+		frappe.db.commit()  # nosemgrep
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js
new file mode 100644
index 0000000..e39b563
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js
@@ -0,0 +1,13 @@
+frappe.listview_settings['BOM Update Log'] = {
+	add_fields: ["status"],
+	get_indicator: function(doc) {
+		let status_map = {
+			"Queued": "orange",
+			"In Progress": "blue",
+			"Completed": "green",
+			"Failed": "red"
+		};
+
+		return [__(doc.status), status_map[doc.status], "status,=," + doc.status];
+	}
+};
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
new file mode 100644
index 0000000..47efea9
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
@@ -0,0 +1,96 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import (
+	BOMMissingError,
+	run_bom_job,
+)
+from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom
+
+test_records = frappe.get_test_records("BOM")
+
+
+class TestBOMUpdateLog(FrappeTestCase):
+	"Test BOM Update Tool Operations via BOM Update Log."
+
+	def setUp(self):
+		bom_doc = frappe.copy_doc(test_records[0])
+		bom_doc.items[1].item_code = "_Test Item"
+		bom_doc.insert()
+
+		self.boms = frappe._dict(
+			current_bom="BOM-_Test Item Home Desktop Manufactured-001",
+			new_bom=bom_doc.name,
+		)
+
+		self.new_bom_doc = bom_doc
+
+	def tearDown(self):
+		frappe.db.rollback()
+
+		if self._testMethodName == "test_bom_update_log_completion":
+			# clear logs and delete BOM created via setUp
+			frappe.db.delete("BOM Update Log")
+			self.new_bom_doc.cancel()
+			self.new_bom_doc.delete()
+
+			# explicitly commit and restore to original state
+			frappe.db.commit()  # nosemgrep
+
+	def test_bom_update_log_validate(self):
+		"Test if BOM presence is validated."
+
+		with self.assertRaises(BOMMissingError):
+			enqueue_replace_bom(boms={})
+
+		with self.assertRaises(frappe.ValidationError):
+			enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom=self.boms.new_bom))
+
+		with self.assertRaises(frappe.ValidationError):
+			enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom="Dummy BOM"))
+
+	def test_bom_update_log_queueing(self):
+		"Test if BOM Update Log is created and queued."
+
+		log = enqueue_replace_bom(
+			boms=self.boms,
+		)
+
+		self.assertEqual(log.docstatus, 1)
+		self.assertEqual(log.status, "Queued")
+
+	def test_bom_update_log_completion(self):
+		"Test if BOM Update Log handles job completion correctly."
+
+		log = enqueue_replace_bom(
+			boms=self.boms,
+		)
+
+		# Explicitly commits log, new bom (setUp) and replacement impact.
+		# Is run via background jobs IRL
+		run_bom_job(
+			doc=log,
+			boms=self.boms,
+			update_type="Replace BOM",
+		)
+		log.reload()
+
+		self.assertEqual(log.status, "Completed")
+
+		# teardown (undo replace impact) due to commit
+		boms = frappe._dict(
+			current_bom=self.boms.new_bom,
+			new_bom=self.boms.current_bom,
+		)
+		log2 = enqueue_replace_bom(
+			boms=self.boms,
+		)
+		run_bom_job(  # Explicitly commits
+			doc=log2,
+			boms=boms,
+			update_type="Replace BOM",
+		)
+		self.assertEqual(log2.status, "Completed")
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js
index bf5fe2e..7ba6517 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js
+++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js
@@ -20,30 +20,67 @@
 
 	refresh: function(frm) {
 		frm.disable_save();
+		frm.events.disable_button(frm, "replace");
+
+		frm.add_custom_button(__("View BOM Update Log"), () => {
+			frappe.set_route("List", "BOM Update Log");
+		});
 	},
 
-	replace: function(frm) {
+	disable_button: (frm, field, disable=true) => {
+		frm.get_field(field).input.disabled = disable;
+	},
+
+	current_bom: (frm) => {
+		if (frm.doc.current_bom && frm.doc.new_bom) {
+			frm.events.disable_button(frm, "replace", false);
+		}
+	},
+
+	new_bom: (frm) => {
+		if (frm.doc.current_bom && frm.doc.new_bom) {
+			frm.events.disable_button(frm, "replace", false);
+		}
+	},
+
+	replace: (frm) => {
 		if (frm.doc.current_bom && frm.doc.new_bom) {
 			frappe.call({
 				method: "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.enqueue_replace_bom",
 				freeze: true,
 				args: {
-					args: {
+					boms: {
 						"current_bom": frm.doc.current_bom,
 						"new_bom": frm.doc.new_bom
 					}
+				},
+				callback: result => {
+					if (result && result.message && !result.exc) {
+						frm.events.confirm_job_start(frm, result.message);
+					}
 				}
 			});
 		}
 	},
 
-	update_latest_price_in_all_boms: function() {
+	update_latest_price_in_all_boms: (frm) => {
 		frappe.call({
 			method: "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.enqueue_update_cost",
 			freeze: true,
-			callback: function() {
-				frappe.msgprint(__("Latest price updated in all BOMs"));
+			callback: result => {
+				if (result && result.message && !result.exc) {
+					frm.events.confirm_job_start(frm, result.message);
+				}
 			}
 		});
+	},
+
+	confirm_job_start: (frm, log_data) => {
+		let log_link = frappe.utils.get_form_link("BOM Update Log", log_data.name, true);
+		frappe.msgprint({
+			"message": __("BOM Updation is queued and may take a few minutes. Check {0} for progress.", [log_link]),
+			"title": __("BOM Update Initiated"),
+			"indicator": "blue"
+		});
 	}
 });
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
index 00711ca..b0e7da1 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
@@ -1,136 +1,69 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
 
-
 import json
+from typing import TYPE_CHECKING, Dict, Literal, Optional, Union
 
-import click
+if TYPE_CHECKING:
+	from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog
+
 import frappe
-from frappe import _
 from frappe.model.document import Document
-from frappe.utils import cstr, flt
 
 from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order
 
 
 class BOMUpdateTool(Document):
-	def replace_bom(self):
-		self.validate_bom()
-
-		unit_cost = get_new_bom_unit_cost(self.new_bom)
-		self.update_new_bom(unit_cost)
-
-		frappe.cache().delete_key("bom_children")
-		bom_list = self.get_parent_boms(self.new_bom)
-
-		with click.progressbar(bom_list) as bom_list:
-			pass
-		for bom in bom_list:
-			try:
-				bom_obj = frappe.get_cached_doc("BOM", bom)
-				# this is only used for versioning and we do not want
-				# to make separate db calls by using load_doc_before_save
-				# which proves to be expensive while doing bulk replace
-				bom_obj._doc_before_save = bom_obj
-				bom_obj.update_new_bom(self.current_bom, self.new_bom, unit_cost)
-				bom_obj.update_exploded_items()
-				bom_obj.calculate_cost()
-				bom_obj.update_parent_cost()
-				bom_obj.db_update()
-				if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version:
-					bom_obj.save_version()
-			except Exception:
-				frappe.log_error(frappe.get_traceback())
-
-	def validate_bom(self):
-		if cstr(self.current_bom) == cstr(self.new_bom):
-			frappe.throw(_("Current BOM and New BOM can not be same"))
-
-		if frappe.db.get_value("BOM", self.current_bom, "item") != frappe.db.get_value(
-			"BOM", self.new_bom, "item"
-		):
-			frappe.throw(_("The selected BOMs are not for the same item"))
-
-	def update_new_bom(self, unit_cost):
-		frappe.db.sql(
-			"""update `tabBOM Item` set bom_no=%s,
-			rate=%s, amount=stock_qty*%s where bom_no = %s and docstatus < 2 and parenttype='BOM'""",
-			(self.new_bom, unit_cost, unit_cost, self.current_bom),
-		)
-
-	def get_parent_boms(self, bom, bom_list=None):
-		if bom_list is None:
-			bom_list = []
-		data = frappe.db.sql(
-			"""SELECT DISTINCT parent FROM `tabBOM Item`
-			WHERE bom_no = %s AND docstatus < 2 AND parenttype='BOM'""",
-			bom,
-		)
-
-		for d in data:
-			if self.new_bom == d[0]:
-				frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(bom, self.new_bom))
-
-			bom_list.append(d[0])
-			self.get_parent_boms(d[0], bom_list)
-
-		return list(set(bom_list))
-
-
-def get_new_bom_unit_cost(bom):
-	new_bom_unitcost = frappe.db.sql(
-		"""SELECT `total_cost`/`quantity`
-		FROM `tabBOM` WHERE name = %s""",
-		bom,
-	)
-
-	return flt(new_bom_unitcost[0][0]) if new_bom_unitcost else 0
+	pass
 
 
 @frappe.whitelist()
-def enqueue_replace_bom(args):
-	if isinstance(args, str):
-		args = json.loads(args)
+def enqueue_replace_bom(
+	boms: Optional[Union[Dict, str]] = None, args: Optional[Union[Dict, str]] = None
+) -> "BOMUpdateLog":
+	"""Returns a BOM Update Log (that queues a job) for BOM Replacement."""
+	boms = boms or args
+	if isinstance(boms, str):
+		boms = json.loads(boms)
 
-	frappe.enqueue(
-		"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom",
-		args=args,
-		timeout=40000,
-	)
-	frappe.msgprint(_("Queued for replacing the BOM. It may take a few minutes."))
+	update_log = create_bom_update_log(boms=boms)
+	return update_log
 
 
 @frappe.whitelist()
-def enqueue_update_cost():
-	frappe.enqueue(
-		"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost", timeout=40000
-	)
-	frappe.msgprint(
-		_("Queued for updating latest price in all Bill of Materials. It may take a few minutes.")
-	)
+def enqueue_update_cost() -> "BOMUpdateLog":
+	"""Returns a BOM Update Log (that queues a job) for BOM Cost Updation."""
+	update_log = create_bom_update_log(update_type="Update Cost")
+	return update_log
 
 
-def update_latest_price_in_all_boms():
+def auto_update_latest_price_in_all_boms() -> None:
+	"""Called via hooks.py."""
 	if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"):
 		update_cost()
 
 
-def replace_bom(args):
-	frappe.db.auto_commit_on_many_writes = 1
-	args = frappe._dict(args)
-
-	doc = frappe.get_doc("BOM Update Tool")
-	doc.current_bom = args.current_bom
-	doc.new_bom = args.new_bom
-	doc.replace_bom()
-
-	frappe.db.auto_commit_on_many_writes = 0
-
-
-def update_cost():
-	frappe.db.auto_commit_on_many_writes = 1
+def update_cost() -> None:
+	"""Updates Cost for all BOMs from bottom to top."""
 	bom_list = get_boms_in_bottom_up_order()
 	for bom in bom_list:
 		frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True)
 
-	frappe.db.auto_commit_on_many_writes = 0
+
+def create_bom_update_log(
+	boms: Optional[Dict[str, str]] = None,
+	update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
+) -> "BOMUpdateLog":
+	"""Creates a BOM Update Log that handles the background job."""
+
+	boms = boms or {}
+	current_bom = boms.get("current_bom")
+	new_bom = boms.get("new_bom")
+	return frappe.get_doc(
+		{
+			"doctype": "BOM Update Log",
+			"current_bom": current_bom,
+			"new_bom": new_bom,
+			"update_type": update_type,
+		}
+	).submit()
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
index 57785e5..fae72a0 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
@@ -4,6 +4,7 @@
 import frappe
 from frappe.tests.utils import FrappeTestCase
 
+from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom
 from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
 from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
 from erpnext.stock.doctype.item.test_item import create_item
@@ -12,6 +13,8 @@
 
 
 class TestBOMUpdateTool(FrappeTestCase):
+	"Test major functions run via BOM Update Tool."
+
 	def test_replace_bom(self):
 		current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
 
@@ -19,18 +22,16 @@
 		bom_doc.items[1].item_code = "_Test Item"
 		bom_doc.insert()
 
-		update_tool = frappe.get_doc("BOM Update Tool")
-		update_tool.current_bom = current_bom
-		update_tool.new_bom = bom_doc.name
-		update_tool.replace_bom()
+		boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name)
+		replace_bom(boms)
 
 		self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom))
 		self.assertTrue(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", bom_doc.name))
 
 		# reverse, as it affects other testcases
-		update_tool.current_bom = bom_doc.name
-		update_tool.new_bom = current_bom
-		update_tool.replace_bom()
+		boms.current_bom = bom_doc.name
+		boms.new_bom = current_bom
+		replace_bom(boms)
 
 	def test_bom_cost(self):
 		for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]:
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index d85b8a6..b2824e1 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -28,12 +28,12 @@
 		frappe.flags.resume_job = 0;
 		let has_items = frm.doc.items && frm.doc.items.length;
 
-		if (frm.doc.__onload.work_order_closed) {
+		if (!frm.is_new() && frm.doc.__onload.work_order_closed) {
 			frm.disable_save();
 			return;
 		}
 
-		if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) {
+		if (!frm.is_new() && has_items && frm.doc.docstatus < 2) {
 			let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
 			let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
 
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 89f9ca6..60b32b8 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -501,7 +501,7 @@
 			po = frappe.new_doc("Purchase Order")
 			po.supplier = supplier
 			po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
-			po.is_subcontracted = "Yes"
+			po.is_subcontracted = 1
 			for row in po_list:
 				po_data = {
 					"item_code": row.production_item,
diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
index c0affd9..ac2f61c 100644
--- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
+++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py
@@ -3,6 +3,7 @@
 
 
 import frappe
+from frappe import _
 
 
 def execute(filters=None):
@@ -46,17 +47,22 @@
 def get_columns():
 	return [
 		{
-			"label": "Item Code",
+			"label": _("Item Code"),
 			"fieldtype": "Link",
 			"fieldname": "item_code",
 			"width": 300,
 			"options": "Item",
 		},
-		{"label": "Item Name", "fieldtype": "data", "fieldname": "item_name", "width": 100},
-		{"label": "BOM", "fieldtype": "Link", "fieldname": "bom", "width": 150, "options": "BOM"},
-		{"label": "Qty", "fieldtype": "data", "fieldname": "qty", "width": 100},
-		{"label": "UOM", "fieldtype": "data", "fieldname": "uom", "width": 100},
-		{"label": "BOM Level", "fieldtype": "Int", "fieldname": "bom_level", "width": 100},
-		{"label": "Standard Description", "fieldtype": "data", "fieldname": "description", "width": 150},
-		{"label": "Scrap", "fieldtype": "data", "fieldname": "scrap", "width": 100},
+		{"label": _("Item Name"), "fieldtype": "data", "fieldname": "item_name", "width": 100},
+		{"label": _("BOM"), "fieldtype": "Link", "fieldname": "bom", "width": 150, "options": "BOM"},
+		{"label": _("Qty"), "fieldtype": "data", "fieldname": "qty", "width": 100},
+		{"label": _("UOM"), "fieldtype": "data", "fieldname": "uom", "width": 100},
+		{"label": _("BOM Level"), "fieldtype": "Int", "fieldname": "bom_level", "width": 100},
+		{
+			"label": _("Standard Description"),
+			"fieldtype": "data",
+			"fieldname": "description",
+			"width": 150,
+		},
+		{"label": _("Scrap"), "fieldtype": "data", "fieldname": "scrap", "width": 100},
 	]
diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
index 17f7f5e..2c8f82f 100644
--- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
+++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py
@@ -3,6 +3,7 @@
 
 
 import frappe
+from frappe import _
 from frappe.utils import flt
 
 
@@ -114,28 +115,28 @@
 def get_column(filters):
 	return [
 		{
-			"label": "Finished Good",
+			"label": _("Finished Good"),
 			"fieldtype": "Link",
 			"fieldname": "item_code",
 			"width": 300,
 			"options": "Item",
 		},
-		{"label": "Item Name", "fieldtype": "data", "fieldname": "item_name", "width": 100},
+		{"label": _("Item Name"), "fieldtype": "data", "fieldname": "item_name", "width": 100},
 		{
-			"label": "Document Type",
+			"label": _("Document Type"),
 			"fieldtype": "Link",
 			"fieldname": "document_type",
 			"width": 150,
 			"options": "DocType",
 		},
 		{
-			"label": "Document Name",
+			"label": _("Document Name"),
 			"fieldtype": "Dynamic Link",
 			"fieldname": "document_name",
 			"width": 150,
 		},
-		{"label": "BOM Level", "fieldtype": "Int", "fieldname": "bom_level", "width": 100},
-		{"label": "Order Qty", "fieldtype": "Float", "fieldname": "qty", "width": 120},
-		{"label": "Received Qty", "fieldtype": "Float", "fieldname": "produced_qty", "width": 160},
-		{"label": "Pending Qty", "fieldtype": "Float", "fieldname": "pending_qty", "width": 110},
+		{"label": _("BOM Level"), "fieldtype": "Int", "fieldname": "bom_level", "width": 100},
+		{"label": _("Order Qty"), "fieldtype": "Float", "fieldname": "qty", "width": 120},
+		{"label": _("Received Qty"), "fieldtype": "Float", "fieldname": "produced_qty", "width": 160},
+		{"label": _("Pending Qty"), "fieldtype": "Float", "fieldname": "pending_qty", "width": 110},
 	]
diff --git a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py
index c6b7e58..063ebba 100644
--- a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py
+++ b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py
@@ -3,6 +3,7 @@
 
 
 import frappe
+from frappe import _
 from frappe.utils import cint
 
 
@@ -99,59 +100,65 @@
 	columns = [
 		{
 			"fieldname": "work_order",
-			"label": "Work Order",
+			"label": _("Work Order"),
 			"fieldtype": "Link",
 			"options": "Work Order",
 			"width": 110,
 		},
-		{"fieldname": "bom_no", "label": "BOM", "fieldtype": "Link", "options": "BOM", "width": 120},
+		{"fieldname": "bom_no", "label": _("BOM"), "fieldtype": "Link", "options": "BOM", "width": 120},
 		{
 			"fieldname": "description",
-			"label": "Description",
+			"label": _("Description"),
 			"fieldtype": "Data",
 			"options": "",
 			"width": 230,
 		},
 		{
 			"fieldname": "item_code",
-			"label": "Item Code",
+			"label": _("Item Code"),
 			"fieldtype": "Link",
 			"options": "Item",
 			"width": 110,
 		},
 		{
 			"fieldname": "source_warehouse",
-			"label": "Source Warehouse",
+			"label": _("Source Warehouse"),
 			"fieldtype": "Link",
 			"options": "Warehouse",
 			"width": 110,
 		},
-		{"fieldname": "qty", "label": "Qty to Build", "fieldtype": "Data", "options": "", "width": 110},
-		{"fieldname": "status", "label": "Status", "fieldtype": "Data", "options": "", "width": 100},
+		{
+			"fieldname": "qty",
+			"label": _("Qty to Build"),
+			"fieldtype": "Data",
+			"options": "",
+			"width": 110,
+		},
+		{"fieldname": "status", "label": _("Status"), "fieldtype": "Data", "options": "", "width": 100},
 		{
 			"fieldname": "req_items",
-			"label": "# Req'd Items",
+			"label": _("# Req'd Items"),
 			"fieldtype": "Data",
 			"options": "",
 			"width": 105,
 		},
 		{
 			"fieldname": "instock",
-			"label": "# In Stock",
+			"label": _("# In Stock"),
 			"fieldtype": "Data",
 			"options": "",
 			"width": 105,
 		},
 		{
 			"fieldname": "buildable_qty",
-			"label": "Buildable Qty",
+			"label": _("Buildable Qty"),
 			"fieldtype": "Data",
 			"options": "",
 			"width": 100,
 		},
 		{
 			"fieldname": "ready_to_build",
-			"label": "Build All?",
+			"label": _("Build All?"),
 			"fieldtype": "Data",
 			"options": "",
 			"width": 90,
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 028834a..a3bf78b 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -4,6 +4,7 @@
 erpnext.patches.v13_0.add_bin_unique_constraint
 erpnext.patches.v11_0.refactor_naming_series
 erpnext.patches.v11_0.refactor_autoname_naming
+erpnext.patches.v14_0.change_is_subcontracted_fieldtype
 execute:frappe.reload_doc("accounts", "doctype", "POS Payment Method") #2020-05-28
 execute:frappe.reload_doc("HR", "doctype", "HR Settings") #2020-01-16 #2020-07-24
 erpnext.patches.v4_2.update_requested_and_ordered_qty #2021-03-31
diff --git a/erpnext/patches/v12_0/update_is_cancelled_field.py b/erpnext/patches/v12_0/update_is_cancelled_field.py
index b567823..398dd70 100644
--- a/erpnext/patches/v12_0/update_is_cancelled_field.py
+++ b/erpnext/patches/v12_0/update_is_cancelled_field.py
@@ -20,7 +20,7 @@
 			"""
 				UPDATE `tab{doctype}`
 				SET is_cancelled = 0
-				where is_cancelled in ('', NULL, 'No')""".format(
+				where is_cancelled in ('', 'No') or is_cancelled is NULL""".format(
 				doctype=doctype
 			)
 		)
diff --git a/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py b/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py
new file mode 100644
index 0000000..9b07ba8
--- /dev/null
+++ b/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py
@@ -0,0 +1,26 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+
+
+def execute():
+	for doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation"]:
+		frappe.db.sql(
+			"""
+				UPDATE `tab{doctype}`
+				SET is_subcontracted = 0
+				where is_subcontracted in ('', 'No') or is_subcontracted is null""".format(
+				doctype=doctype
+			)
+		)
+		frappe.db.sql(
+			"""
+				UPDATE `tab{doctype}`
+				SET is_subcontracted = 1
+				where is_subcontracted = 'Yes'""".format(
+				doctype=doctype
+			)
+		)
+
+		frappe.reload_doc(frappe.get_meta(doctype).module, "doctype", frappe.scrub(doctype))
diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py
index 31f26b2..6ec34b9 100644
--- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py
+++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py
@@ -44,8 +44,7 @@
 		if max_benefits < claimed_amount:
 			frappe.throw(
 				_(
-					"Maximum benefit of employee {0} exceeds {1} by the sum {2} of previous claimed\
-			amount"
+					"Maximum benefit of employee {0} exceeds {1} by the sum {2} of previous claimed amount"
 				).format(self.employee, max_benefits, claimed_amount - max_benefits)
 			)
 
@@ -84,8 +83,7 @@
 		if max_benefits < pro_rata_amount + claimed_amount:
 			frappe.throw(
 				_(
-					"Maximum benefit of employee {0} exceeds {1} by the sum {2} of benefit application pro-rata component\
-			amount and previous claimed amount"
+					"Maximum benefit of employee {0} exceeds {1} by the sum {2} of benefit application pro-rata component amount and previous claimed amount"
 				).format(
 					self.employee, max_benefits, pro_rata_amount + claimed_amount - max_benefits
 				)
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js
index 014a121..7290a9e 100644
--- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js
@@ -34,7 +34,7 @@
 	to_year(frm, cdt, cdn) {
 		let row = locals[cdt][cdn];
 		if (row.to_year <= row.from_year && row.to_year === 0) {
-			frappe.throw(__("To(Year) year can not be less than From(year) "));
+			frappe.throw(__("To(Year) year can not be less than From(year)"));
 		}
 	}
 });
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index 496c37b..62e183e 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -112,7 +112,7 @@
 			},
 			callback: function (r) {
 				if (r.message && !r.message.submitted) {
-					frm.add_custom_button("Make Bank Entry", function () {
+					frm.add_custom_button(__("Make Bank Entry"), function () {
 						make_bank_entry(frm);
 					}).addClass("btn-primary");
 				}
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index e1d1fa1..dbeadc5 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -1290,7 +1290,16 @@
 	return salary_date
 
 
-def make_leave_application(employee, from_date, to_date, leave_type, company=None, submit=True):
+def make_leave_application(
+	employee,
+	from_date,
+	to_date,
+	leave_type,
+	company=None,
+	half_day=False,
+	half_day_date=None,
+	submit=True,
+):
 	leave_application = frappe.get_doc(
 		dict(
 			doctype="Leave Application",
@@ -1298,6 +1307,8 @@
 			leave_type=leave_type,
 			from_date=from_date,
 			to_date=to_date,
+			half_day=half_day,
+			half_day_date=half_day_date,
 			company=company or erpnext.get_default_company() or "_Test Company",
 			status="Approved",
 			leave_approver="test@example.com",
diff --git a/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py
index e5348df..4223f9d 100644
--- a/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py
+++ b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py
@@ -142,21 +142,21 @@
 	return [
 		{
 			"value": gross_pay,
-			"label": "Total Gross Pay",
+			"label": _("Total Gross Pay"),
 			"indicator": "Green",
 			"datatype": "Currency",
 			"currency": currency,
 		},
 		{
 			"value": total_deductions,
-			"label": "Total Deduction",
+			"label": _("Total Deduction"),
 			"datatype": "Currency",
 			"indicator": "Red",
 			"currency": currency,
 		},
 		{
 			"value": net_pay,
-			"label": "Total Net Pay",
+			"label": _("Total Net Pay"),
 			"datatype": "Currency",
 			"indicator": "Blue",
 			"currency": currency,
diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py
index 5c3dc2d..17e3155 100644
--- a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py
+++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py
@@ -3,6 +3,7 @@
 
 
 import frappe
+from frappe import _
 from frappe.utils import date_diff, nowdate
 
 
@@ -83,19 +84,24 @@
 
 def get_columns():
 	columns = [
-		{"fieldname": "name", "fieldtype": "Link", "label": "Task", "options": "Task", "width": 150},
-		{"fieldname": "subject", "fieldtype": "Data", "label": "Subject", "width": 200},
-		{"fieldname": "status", "fieldtype": "Data", "label": "Status", "width": 100},
-		{"fieldname": "priority", "fieldtype": "Data", "label": "Priority", "width": 80},
-		{"fieldname": "progress", "fieldtype": "Data", "label": "Progress (%)", "width": 120},
+		{"fieldname": "name", "fieldtype": "Link", "label": _("Task"), "options": "Task", "width": 150},
+		{"fieldname": "subject", "fieldtype": "Data", "label": _("Subject"), "width": 200},
+		{"fieldname": "status", "fieldtype": "Data", "label": _("Status"), "width": 100},
+		{"fieldname": "priority", "fieldtype": "Data", "label": _("Priority"), "width": 80},
+		{"fieldname": "progress", "fieldtype": "Data", "label": _("Progress (%)"), "width": 120},
 		{
 			"fieldname": "exp_start_date",
 			"fieldtype": "Date",
-			"label": "Expected Start Date",
+			"label": _("Expected Start Date"),
 			"width": 150,
 		},
-		{"fieldname": "exp_end_date", "fieldtype": "Date", "label": "Expected End Date", "width": 150},
-		{"fieldname": "completed_on", "fieldtype": "Date", "label": "Actual End Date", "width": 130},
-		{"fieldname": "delay", "fieldtype": "Data", "label": "Delay (In Days)", "width": 120},
+		{
+			"fieldname": "exp_end_date",
+			"fieldtype": "Date",
+			"label": _("Expected End Date"),
+			"width": 150,
+		},
+		{"fieldname": "completed_on", "fieldtype": "Date", "label": _("Actual End Date"), "width": 130},
+		{"fieldname": "delay", "fieldtype": "Data", "label": _("Delay (In Days)"), "width": 120},
 	]
 	return columns
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 54e5daa..bbf1ff6 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -81,7 +81,7 @@
 		}
 
 		this.frm.set_query("item_code", "items", function() {
-			if (me.frm.doc.is_subcontracted == "Yes") {
+			if (me.frm.doc.is_subcontracted) {
 				return{
 					query: "erpnext.controllers.queries.item_query",
 					filters:{ 'supplier': me.frm.doc.supplier, 'is_sub_contracted_item': 1 }
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 23c2bd4..fa41e1b 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -239,7 +239,7 @@
 				() => set_value('currency', currency),
 				() => set_value('price_list_currency', currency),
 				() => set_value('status', 'Draft'),
-				() => set_value('is_subcontracted', 'No'),
+				() => set_value('is_subcontracted', 0),
 				() => {
 					if(this.frm.doc.company && !this.frm.doc.amended_from) {
 						this.frm.trigger("company");
@@ -403,17 +403,6 @@
 		var sms_man = new erpnext.SMSManager(this.frm.doc);
 	}
 
-	barcode(doc, cdt, cdn) {
-		const d = locals[cdt][cdn];
-		if (!d.barcode) {
-			// barcode cleared, remove item
-			d.item_code = "";
-		}
-		// flag required for circular triggers
-		d._triggerd_from_barcode = true;
-		this.item_code(doc, cdt, cdn);
-	}
-
 	item_code(doc, cdt, cdn) {
 		var me = this;
 		var item = frappe.get_doc(cdt, cdn);
@@ -431,9 +420,7 @@
 			this.frm.doc.doctype === 'Delivery Note') {
 			show_batch_dialog = 1;
 		}
-		if (!item._triggerd_from_barcode) {
-			item.barcode = null;
-		}
+		item.barcode = null;
 
 
 		if(item.item_code || item.barcode || item.serial_no) {
@@ -539,6 +526,12 @@
 											if(!d[k]) d[k] = v;
 										});
 
+										if (d.__disable_batch_serial_selector) {
+											// reset for future use.
+											d.__disable_batch_serial_selector = false;
+											return;
+										}
+
 										if (d.has_batch_no && d.has_serial_no) {
 											d.batch_no = undefined;
 										}
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 9339c5d..eded165 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -483,7 +483,7 @@
 			if (frm.doc.doctype == 'Sales Order') {
 				filters = {"is_sales_item": 1};
 			} else if (frm.doc.doctype == 'Purchase Order') {
-				if (frm.doc.is_subcontracted == "Yes") {
+				if (frm.doc.is_subcontracted) {
 					filters = {"is_sub_contracted_item": 1};
 				} else {
 					filters = {"is_purchase_item": 1};
diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js
index abea5fc..f72b85c 100644
--- a/erpnext/public/js/utils/barcode_scanner.js
+++ b/erpnext/public/js/utils/barcode_scanner.js
@@ -21,9 +21,7 @@
 		//     batch_no: "LOT12", // present if batch was scanned
 		//     serial_no: "987XYZ", // present if serial no was scanned
 		// }
-		this.scan_api =
-			opts.scan_api ||
-			"erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number";
+		this.scan_api = opts.scan_api || "erpnext.stock.utils.scan_barcode";
 	}
 
 	process_scan() {
@@ -52,14 +50,16 @@
 					return;
 				}
 
-				me.update_table(data.item_code, data.barcode, data.batch_no, data.serial_no);
+				me.update_table(data);
 			});
 	}
 
-	update_table(item_code, barcode, batch_no, serial_no) {
+	update_table(data) {
 		let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
 		let row = null;
 
+		const {item_code, barcode, batch_no, serial_no} = data;
+
 		// Check if batch is scanned and table has batch no field
 		let batch_no_scan =
 			Boolean(batch_no) && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field);
@@ -68,7 +68,7 @@
 			row = this.get_batch_row_to_modify(batch_no);
 		} else {
 			// serial or barcode scan
-			row = this.get_row_to_modify_on_scan(row, item_code);
+			row = this.get_row_to_modify_on_scan(item_code);
 		}
 
 		if (!row) {
@@ -84,6 +84,7 @@
 		}
 
 		this.show_scan_message(row.idx, row.item_code);
+		this.set_selector_trigger_flag(row, data);
 		this.set_item(row, item_code);
 		this.set_serial_no(row, serial_no);
 		this.set_batch_no(row, batch_no);
@@ -91,6 +92,19 @@
 		this.clean_up();
 	}
 
+	// batch and serial selector is reduandant when all info can be added by scan
+	// this flag on item row is used by transaction.js to avoid triggering selector
+	set_selector_trigger_flag(row, data) {
+		const {batch_no, serial_no, has_batch_no, has_serial_no} = data;
+
+		const require_selecting_batch = has_batch_no && !batch_no;
+		const require_selecting_serial = has_serial_no && !serial_no;
+
+		if (!(require_selecting_batch || require_selecting_serial)) {
+			row.__disable_batch_serial_selector = true;
+		}
+	}
+
 	set_item(row, item_code) {
 		const item_data = { item_code: item_code };
 		item_data[this.qty_field] = (row[this.qty_field] || 0) + 1;
@@ -163,21 +177,17 @@
 	get_batch_row_to_modify(batch_no) {
 		// get row if batch already exists in table
 		const existing_batch_row = this.items_table.find((d) => d.batch_no === batch_no);
-		return existing_batch_row || null;
+		return existing_batch_row || this.get_existing_blank_row();
 	}
 
-	get_row_to_modify_on_scan(row_to_modify, item_code) {
+	get_row_to_modify_on_scan(item_code) {
 		// get an existing item row to increment or blank row to modify
 		const existing_item_row = this.items_table.find((d) => d.item_code === item_code);
-		const blank_item_row = this.items_table.find((d) => !d.item_code);
+		return existing_item_row || this.get_existing_blank_row();
+	}
 
-		if (existing_item_row) {
-			row_to_modify = existing_item_row;
-		} else if (blank_item_row) {
-			row_to_modify = blank_item_row;
-		}
-
-		return row_to_modify;
+	get_existing_blank_row() {
+		return this.items_table.find((d) => !d.item_code);
 	}
 
 	clean_up() {
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index f484545..64c5ee5 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -609,8 +609,8 @@
 		&& erpnext.stock.bom
 		&& erpnext.stock.bom.name === doc.bom_no;
 	const itemChecks = !!item
-		&& !item.allow_alternative_item
-		&& erpnext.stock.bom && erpnext.stock.items
+		&& !item.original_item
+		&& erpnext.stock.bom && erpnext.stock.bom.items
 		&& (item.item_code in erpnext.stock.bom.items);
 	return docChecks && itemChecks;
 }
diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.js b/erpnext/regional/doctype/datev_settings/datev_settings.js
index f047059..3c36549 100644
--- a/erpnext/regional/doctype/datev_settings/datev_settings.js
+++ b/erpnext/regional/doctype/datev_settings/datev_settings.js
@@ -3,6 +3,6 @@
 
 frappe.ui.form.on('DATEV Settings', {
 	refresh: function(frm) {
-		frm.add_custom_button('Show Report', () => frappe.set_route('query-report', 'DATEV'), "fa fa-table");
+		frm.add_custom_button(__('Show Report'), () => frappe.set_route('query-report', 'DATEV'), "fa fa-table");
 	}
 });
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 348f0c6..17b018c 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -105,6 +105,30 @@
 						},
 						primary_action_label: __('Submit')
 					});
+					d.fields_dict.transporter.df.onchange = function () {
+						const transporter = d.fields_dict.transporter.value;
+						if (transporter) {
+							frappe.db.get_value('Supplier', transporter, ['gst_transporter_id', 'supplier_name'])
+								.then(({ message }) => {
+									d.set_value('gst_transporter_id', message.gst_transporter_id);
+									d.set_value('transporter_name', message.supplier_name);
+								});
+						} else {
+							d.set_value('gst_transporter_id', '');
+							d.set_value('transporter_name', '');
+						}
+					};
+					d.fields_dict.driver.df.onchange = function () {
+						const driver = d.fields_dict.driver.value;
+						if (driver) {
+							frappe.db.get_value('Driver', driver, ['full_name'])
+								.then(({ message }) => {
+									d.set_value('driver_name', message.full_name);
+								});
+						} else {
+							d.set_value('driver_name', '');
+						}
+					};
 					d.show();
 				};
 
@@ -153,7 +177,6 @@
 			'fieldname': 'gst_transporter_id',
 			'label': 'GST Transporter ID',
 			'fieldtype': 'Data',
-			'fetch_from': 'transporter.gst_transporter_id',
 			'default': frm.doc.gst_transporter_id
 		},
 		{
@@ -189,9 +212,9 @@
 			'fieldname': 'transporter_name',
 			'label': 'Transporter Name',
 			'fieldtype': 'Data',
-			'fetch_from': 'transporter.name',
 			'read_only': 1,
-			'default': frm.doc.transporter_name
+			'default': frm.doc.transporter_name,
+			'depends_on': 'transporter'
 		},
 		{
 			'fieldname': 'mode_of_transport',
@@ -206,7 +229,8 @@
 			'fieldtype': 'Data',
 			'fetch_from': 'driver.full_name',
 			'read_only': 1,
-			'default': frm.doc.driver_name
+			'default': frm.doc.driver_name,
+			'depends_on': 'driver'
 		},
 		{
 			'fieldname': 'lr_date',
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index cbdec56..8fd9c1c 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -387,7 +387,7 @@
 
 def get_payment_details(invoice):
 	payee_name = invoice.company
-	mode_of_payment = ", ".join([d.mode_of_payment for d in invoice.payments])
+	mode_of_payment = ""
 	paid_amount = invoice.base_paid_amount
 	outstanding_amount = invoice.outstanding_amount
 
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 8189d0a..249c1b7 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -268,6 +268,7 @@
 
 	if tax_template_by_category:
 		party_details["taxes_and_charges"] = tax_template_by_category
+		party_details["taxes"] = get_taxes_and_charges(master_doctype, tax_template_by_category)
 		return party_details
 
 	if not party_details.place_of_supply:
@@ -292,7 +293,7 @@
 		return party_details
 
 	party_details["taxes_and_charges"] = default_tax
-	party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
+	party_details["taxes"] = get_taxes_and_charges(master_doctype, default_tax)
 
 	return party_details
 
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 2e5cbb8..8889a5f 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -100,7 +100,8 @@
 	@frappe.whitelist()
 	def get_customer_group_details(self):
 		doc = frappe.get_doc("Customer Group", self.customer_group)
-		self.accounts = self.credit_limits = []
+		self.accounts = []
+		self.credit_limits = []
 		self.payment_terms = self.default_price_list = ""
 
 		tables = [["accounts", "account"], ["credit_limits", "credit_limit"]]
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 4027d2e..36ca2b2 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -45,7 +45,8 @@
 		c_doc.customer_name = "Testing Customer"
 		c_doc.customer_group = "_Testing Customer Group"
 		c_doc.payment_terms = c_doc.default_price_list = ""
-		c_doc.accounts = c_doc.credit_limits = []
+		c_doc.accounts = []
+		c_doc.credit_limits = []
 		c_doc.insert()
 		c_doc.get_customer_group_details()
 		self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3")
diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json
index ee5b0ea..0318b70 100644
--- a/erpnext/selling/doctype/quotation/quotation.json
+++ b/erpnext/selling/doctype/quotation/quotation.json
@@ -41,6 +41,8 @@
   "price_list_currency",
   "plc_conversion_rate",
   "ignore_pricing_rule",
+  "section_break_33",
+  "scan_barcode",
   "items_section",
   "items",
   "bundle_items_section",
@@ -955,13 +957,23 @@
    "label": "Competitors",
    "options": "Competitor Detail",
    "read_only": 1
+  },
+  {
+   "fieldname": "section_break_33",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "scan_barcode",
+   "fieldtype": "Data",
+   "label": "Scan Barcode",
+   "options": "Barcode"
   }
  ],
  "icon": "fa fa-shopping-cart",
  "idx": 82,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-11-30 01:33:21.106073",
+ "modified": "2022-04-07 11:01:31.157084",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Quotation",
@@ -1056,6 +1068,7 @@
  "show_name_in_global_search": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "timeline_field": "party_name",
  "title_field": "title"
 }
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 87f277f..0b48f70 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -727,7 +727,7 @@
 					args: {
 						reference_doctype: me.frm.doctype,
 						reference_name: me.frm.docname,
-						content: __('Reason for hold: ')+data.reason_for_hold,
+						content: __('Reason for hold:') + ' ' + data.reason_for_hold,
 						comment_email: frappe.session.user,
 						comment_by: frappe.session.user_fullname
 					},
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index bf62982..99afe81 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -3,12 +3,14 @@
 
 
 import json
+from typing import Dict, Optional
 
 import frappe
 from frappe.utils.nestedset import get_root_of
 
 from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
 from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups
+from erpnext.stock.utils import scan_barcode
 
 
 def search_by_term(search_term, warehouse, price_list):
@@ -150,29 +152,8 @@
 
 
 @frappe.whitelist()
-def search_for_serial_or_batch_or_barcode_number(search_value):
-	# search barcode no
-	barcode_data = frappe.db.get_value(
-		"Item Barcode", {"barcode": search_value}, ["barcode", "parent as item_code"], as_dict=True
-	)
-	if barcode_data:
-		return barcode_data
-
-	# search serial no
-	serial_no_data = frappe.db.get_value(
-		"Serial No", search_value, ["name as serial_no", "item_code"], as_dict=True
-	)
-	if serial_no_data:
-		return serial_no_data
-
-	# search batch no
-	batch_no_data = frappe.db.get_value(
-		"Batch", search_value, ["name as batch_no", "item as item_code"], as_dict=True
-	)
-	if batch_no_data:
-		return batch_no_data
-
-	return {}
+def search_for_serial_or_batch_or_barcode_number(search_value: str) -> Dict[str, Optional[str]]:
+	return scan_barcode(search_value)
 
 
 def get_conditions(search_term):
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 6974bed..65e0cbb 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -721,11 +721,14 @@
 
 	async save_and_checkout() {
 		if (this.frm.is_dirty()) {
+			let save_error = false;
+			await this.frm.save(null, null, null, () => save_error = true);
 			// only move to payment section if save is successful
-			frappe.route_hooks.after_save = () => this.payment.checkout();
-			return this.frm.save(
-				null, null, null, () => this.cart.toggle_checkout_btn(true) // show checkout button on error
-			);
+			!save_error && this.payment.checkout();
+			// show checkout button on error
+			save_error && setTimeout(() => {
+				this.cart.toggle_checkout_btn(true);
+			}, 300); // wait for save to finish
 		} else {
 			this.payment.checkout();
 		}
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 1177615..b62b27b 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -243,7 +243,7 @@
 				value: "+1",
 				item: { item_code, batch_no, serial_no, uom, rate }
 			});
-			me.set_search_value('');
+			me.search_field.set_focus();
 		});
 
 		this.search_field.$input.on('input', (e) => {
@@ -328,6 +328,7 @@
 
 	add_filtered_item_to_cart() {
 		this.$items_container.find(".item-wrapper").click();
+		this.set_search_value('');
 	}
 
 	resize_selector(minimize) {
diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py
index c626f5b..6b33a71 100644
--- a/erpnext/selling/page/sales_funnel/sales_funnel.py
+++ b/erpnext/selling/page/sales_funnel/sales_funnel.py
@@ -1,10 +1,11 @@
 # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
+from itertools import groupby
 
 import frappe
-import pandas as pd
 from frappe import _
+from frappe.utils import flt
 
 from erpnext.accounts.report.utils import convert
 
@@ -89,28 +90,21 @@
 			for x in opportunities
 		]
 
-		df = (
-			pd.DataFrame(cp_opportunities)
-			.groupby(["source", "sales_stage"], as_index=False)
-			.agg({"compound_amount": "sum"})
-		)
+		summary = {}
+		sales_stages = set()
+		group_key = lambda o: (o["source"], o["sales_stage"])  # noqa
+		for (source, sales_stage), rows in groupby(cp_opportunities, group_key):
+			summary.setdefault(source, {})[sales_stage] = sum(r["compound_amount"] for r in rows)
+			sales_stages.add(sales_stage)
 
-		result = {}
-		result["labels"] = list(set(df.source.values))
-		result["datasets"] = []
+		pivot_table = []
+		for sales_stage in sales_stages:
+			row = []
+			for source, sales_stage_values in summary.items():
+				row.append(flt(sales_stage_values.get(sales_stage)))
+			pivot_table.append({"chartType": "bar", "name": sales_stage, "values": row})
 
-		for s in set(df.sales_stage.values):
-			result["datasets"].append(
-				{"name": s, "values": [0] * len(result["labels"]), "chartType": "bar"}
-			)
-
-		for row in df.itertuples():
-			source_index = result["labels"].index(row.source)
-
-			for dataset in result["datasets"]:
-				if dataset["name"] == row.sales_stage:
-					dataset["values"][source_index] = row.compound_amount
-
+		result = {"datasets": pivot_table, "labels": list(summary.keys())}
 		return result
 
 	else:
@@ -148,20 +142,14 @@
 			for x in opportunities
 		]
 
-		df = (
-			pd.DataFrame(cp_opportunities)
-			.groupby(["sales_stage"], as_index=True)
-			.agg({"compound_amount": "sum"})
-			.to_dict()
-		)
+		summary = {}
+		for sales_stage, rows in groupby(cp_opportunities, lambda o: o["sales_stage"]):
+			summary[sales_stage] = sum(flt(r["compound_amount"]) for r in rows)
 
-		result = {}
-		result["labels"] = df["compound_amount"].keys()
-		result["datasets"] = []
-		result["datasets"].append(
-			{"name": _("Total Amount"), "values": df["compound_amount"].values(), "chartType": "bar"}
-		)
-
+		result = {
+			"labels": list(summary.keys()),
+			"datasets": [{"name": _("Total Amount"), "values": list(summary.values()), "chartType": "bar"}],
+		}
 		return result
 
 	else:
diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py
index 33badc3..3e4bfb2 100644
--- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py
+++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py
@@ -102,7 +102,7 @@
 def get_data_by_territory(filters, common_columns):
 	columns = [
 		{
-			"label": "Territory",
+			"label": _("Territory"),
 			"fieldname": "territory",
 			"fieldtype": "Link",
 			"options": "Territory",
diff --git a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py
index 1c10a37..98633cb 100644
--- a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py
+++ b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py
@@ -65,7 +65,7 @@
 		_("Credit Limit") + ":Currency:120",
 		_("Outstanding Amt") + ":Currency:100",
 		_("Credit Balance") + ":Currency:120",
-		_("Bypass credit check at Sales Order ") + ":Check:80",
+		_("Bypass credit check at Sales Order") + ":Check:80",
 		_("Is Frozen") + ":Check:80",
 		_("Disabled") + ":Check:80",
 	]
diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
index 12ca7b3..091c20c 100644
--- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
+++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
@@ -235,7 +235,7 @@
 	return {
 		"data": {
 			"labels": labels[:30],  # show max of 30 items in chart
-			"datasets": [{"name": _(" Total Sales Amount"), "values": datapoints[:30]}],
+			"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}],
 		},
 		"type": "bar",
 	}
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
index 0e36b3f..c068ae3 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js
@@ -27,28 +27,55 @@
 			"default": frappe.datetime.get_today()
 		},
 		{
-			"fieldname":"sales_order",
-			"label": __("Sales Order"),
-			"fieldtype": "MultiSelectList",
+			"fieldname":"customer_group",
+			"label": __("Customer Group"),
+			"fieldtype": "Link",
 			"width": 100,
-			"options": "Sales Order",
-			"get_data": function(txt) {
-				return frappe.db.get_link_options("Sales Order", txt, this.filters());
-			},
-			"filters": () => {
-				return {
-					docstatus: 1,
-					payment_terms_template: ['not in', ['']],
-					company: frappe.query_report.get_filter_value("company"),
-					transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]]
+			"options": "Customer Group",
+		},
+		{
+			"fieldname":"customer",
+			"label": __("Customer"),
+			"fieldtype": "Link",
+			"width": 100,
+			"options": "Customer",
+			"get_query": () => {
+				var customer_group = frappe.query_report.get_filter_value('customer_group');
+				return{
+					"query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items",
+					"filters": [
+						['Customer', 'disabled', '=', '0'],
+						['Customer Group','name', '=', customer_group]
+					]
 				}
-			},
-			on_change: function(){
-				frappe.query_report.refresh();
+			}
+		},
+		{
+			"fieldname":"item_group",
+			"label": __("Item Group"),
+			"fieldtype": "Link",
+			"width": 100,
+			"options": "Item Group",
+
+		},
+		{
+			"fieldname":"item",
+			"label": __("Item"),
+			"fieldtype": "Link",
+			"width": 100,
+			"options": "Item",
+			"get_query": () => {
+				var item_group = frappe.query_report.get_filter_value('item_group');
+				return{
+					"query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items",
+					"filters": [
+						['Item', 'disabled', '=', '0'],
+						['Item Group','name', '=', item_group]
+					]
+				}
 			}
 		}
 	]
-
 	return filters;
 }
 
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
index 7f797f6..cb22fb6 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
@@ -3,7 +3,7 @@
 
 import frappe
 from frappe import _, qb, query_builder
-from frappe.query_builder import functions
+from frappe.query_builder import Criterion, functions
 
 
 def get_columns():
@@ -15,6 +15,12 @@
 			"options": "Sales Order",
 		},
 		{
+			"label": _("Customer"),
+			"fieldname": "customer",
+			"fieldtype": "Link",
+			"options": "Customer",
+		},
+		{
 			"label": _("Posting Date"),
 			"fieldname": "submitted",
 			"fieldtype": "Date",
@@ -67,6 +73,55 @@
 	return columns
 
 
+def get_descendants_of(doctype, group_name):
+	group_doc = qb.DocType(doctype)
+	# get lft and rgt of group node
+	lft, rgt = (
+		qb.from_(group_doc).select(group_doc.lft, group_doc.rgt).where(group_doc.name == group_name)
+	).run()[0]
+
+	# get all children of group node
+	query = (
+		qb.from_(group_doc).select(group_doc.name).where((group_doc.lft >= lft) & (group_doc.rgt <= rgt))
+	)
+
+	child_nodes = []
+	for x in query.run():
+		child_nodes.append(x[0])
+
+	return child_nodes
+
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_customers_or_items(doctype, txt, searchfield, start, page_len, filters):
+	filter_list = []
+	if isinstance(filters, list):
+		for item in filters:
+			if item[0] == doctype:
+				filter_list.append(item)
+			elif item[0] == "Customer Group":
+				if item[3] != "":
+					filter_list.append(
+						[doctype, "customer_group", "in", get_descendants_of("Customer Group", item[3])]
+					)
+			elif item[0] == "Item Group":
+				if item[3] != "":
+					filter_list.append([doctype, "item_group", "in", get_descendants_of("Item Group", item[3])])
+
+	if searchfield and txt:
+		filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt])
+
+	return frappe.desk.reportview.execute(
+		doctype,
+		filters=filter_list,
+		fields=["name", "customer_group"] if doctype == "Customer" else ["name", "item_group"],
+		limit_start=start,
+		limit_page_length=page_len,
+		as_list=True,
+	)
+
+
 def get_conditions(filters):
 	"""
 	Convert filter options to conditions used in query
@@ -79,11 +134,37 @@
 	conditions.start_date = filters.period_start_date or frappe.utils.add_months(
 		conditions.end_date, -1
 	)
-	conditions.sales_order = filters.sales_order or []
 
 	return conditions
 
 
+def build_filter_criterions(filters):
+	filters = frappe._dict(filters) if filters else frappe._dict({})
+	qb_criterions = []
+
+	if filters.customer_group:
+		qb_criterions.append(
+			qb.DocType("Sales Order").customer_group.isin(
+				get_descendants_of("Customer Group", filters.customer_group)
+			)
+		)
+
+	if filters.customer:
+		qb_criterions.append(qb.DocType("Sales Order").customer == filters.customer)
+
+	if filters.item_group:
+		qb_criterions.append(
+			qb.DocType("Sales Order Item").item_group.isin(
+				get_descendants_of("Item Group", filters.item_group)
+			)
+		)
+
+	if filters.item:
+		qb_criterions.append(qb.DocType("Sales Order Item").item_code == filters.item)
+
+	return qb_criterions
+
+
 def get_so_with_invoices(filters):
 	"""
 	Get Sales Order with payment terms template with their associated Invoices
@@ -92,16 +173,23 @@
 
 	so = qb.DocType("Sales Order")
 	ps = qb.DocType("Payment Schedule")
+	soi = qb.DocType("Sales Order Item")
+
+	conditions = get_conditions(filters)
+	filter_criterions = build_filter_criterions(filters)
+
 	datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"])
 	ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
 
-	conditions = get_conditions(filters)
 	query_so = (
 		qb.from_(so)
+		.join(soi)
+		.on(soi.parent == so.name)
 		.join(ps)
 		.on(ps.parent == so.name)
 		.select(
 			so.name,
+			so.customer,
 			so.transaction_date.as_("submitted"),
 			ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
 			ps.payment_term,
@@ -117,12 +205,10 @@
 			& (so.company == conditions.company)
 			& (so.transaction_date[conditions.start_date : conditions.end_date])
 		)
+		.where(Criterion.all(filter_criterions))
 		.orderby(so.name, so.transaction_date, ps.due_date)
 	)
 
-	if conditions.sales_order != []:
-		query_so = query_so.where(so.name.isin(conditions.sales_order))
-
 	sorders = query_so.run(as_dict=True)
 
 	invoices = []
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
index 89940a6..9d542f5 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
@@ -11,10 +11,13 @@
 )
 from erpnext.stock.doctype.item.test_item import create_item
 
-test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"]
+test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template", "Customer"]
 
 
 class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
+	def tearDown(self):
+		frappe.db.rollback()
+
 	def create_payment_terms_template(self):
 		# create template for 50-50 payments
 		template = None
@@ -48,9 +51,9 @@
 			template.insert()
 		self.template = template
 
-	def test_payment_terms_status(self):
+	def test_01_payment_terms_status(self):
 		self.create_payment_terms_template()
-		item = create_item(item_code="_Test Excavator", is_stock_item=0)
+		item = create_item(item_code="_Test Excavator 1", is_stock_item=0)
 		so = make_sales_order(
 			transaction_date="2021-06-15",
 			delivery_date=add_days("2021-06-15", -30),
@@ -78,13 +81,14 @@
 				"company": "_Test Company",
 				"period_start_date": "2021-06-01",
 				"period_end_date": "2021-06-30",
-				"sales_order": [so.name],
+				"item": item.item_code,
 			}
 		)
 
 		expected_value = [
 			{
 				"name": so.name,
+				"customer": so.customer,
 				"submitted": datetime.date(2021, 6, 15),
 				"status": "Completed",
 				"payment_term": None,
@@ -98,6 +102,7 @@
 			},
 			{
 				"name": so.name,
+				"customer": so.customer,
 				"submitted": datetime.date(2021, 6, 15),
 				"status": "Partly Paid",
 				"payment_term": None,
@@ -132,11 +137,11 @@
 			)
 			doc.insert()
 
-	def test_alternate_currency(self):
+	def test_02_alternate_currency(self):
 		transaction_date = "2021-06-15"
 		self.create_payment_terms_template()
 		self.create_exchange_rate(transaction_date)
-		item = create_item(item_code="_Test Excavator", is_stock_item=0)
+		item = create_item(item_code="_Test Excavator 2", is_stock_item=0)
 		so = make_sales_order(
 			transaction_date=transaction_date,
 			currency="USD",
@@ -166,7 +171,7 @@
 				"company": "_Test Company",
 				"period_start_date": "2021-06-01",
 				"period_end_date": "2021-06-30",
-				"sales_order": [so.name],
+				"item": item.item_code,
 			}
 		)
 
@@ -174,6 +179,7 @@
 		expected_value = [
 			{
 				"name": so.name,
+				"customer": so.customer,
 				"submitted": datetime.date(2021, 6, 15),
 				"status": "Completed",
 				"payment_term": None,
@@ -187,6 +193,7 @@
 			},
 			{
 				"name": so.name,
+				"customer": so.customer,
 				"submitted": datetime.date(2021, 6, 15),
 				"status": "Partly Paid",
 				"payment_term": None,
@@ -200,3 +207,134 @@
 			},
 		]
 		self.assertEqual(data, expected_value)
+
+	def test_03_group_filters(self):
+		transaction_date = "2021-06-15"
+		self.create_payment_terms_template()
+		item1 = create_item(item_code="_Test Excavator 1", is_stock_item=0)
+		item1.item_group = "Products"
+		item1.save()
+
+		so1 = make_sales_order(
+			transaction_date=transaction_date,
+			delivery_date=add_days(transaction_date, -30),
+			item=item1.item_code,
+			qty=1,
+			rate=1000000,
+			do_not_save=True,
+		)
+		so1.po_no = ""
+		so1.taxes_and_charges = ""
+		so1.taxes = ""
+		so1.payment_terms_template = self.template.name
+		so1.save()
+		so1.submit()
+
+		item2 = create_item(item_code="_Test Steel", is_stock_item=0)
+		item2.item_group = "Raw Material"
+		item2.save()
+
+		so2 = make_sales_order(
+			customer="_Test Customer 1",
+			transaction_date=transaction_date,
+			delivery_date=add_days(transaction_date, -30),
+			item=item2.item_code,
+			qty=100,
+			rate=1000,
+			do_not_save=True,
+		)
+		so2.po_no = ""
+		so2.taxes_and_charges = ""
+		so2.taxes = ""
+		so2.payment_terms_template = self.template.name
+		so2.save()
+		so2.submit()
+
+		base_filters = {
+			"company": "_Test Company",
+			"period_start_date": "2021-06-01",
+			"period_end_date": "2021-06-30",
+		}
+
+		expected_value_so1 = [
+			{
+				"name": so1.name,
+				"customer": so1.customer,
+				"submitted": datetime.date(2021, 6, 15),
+				"status": "Overdue",
+				"payment_term": None,
+				"description": "_Test 50-50",
+				"due_date": datetime.date(2021, 6, 30),
+				"invoice_portion": 50.0,
+				"currency": "INR",
+				"base_payment_amount": 500000.0,
+				"paid_amount": 0.0,
+				"invoices": "",
+			},
+			{
+				"name": so1.name,
+				"customer": so1.customer,
+				"submitted": datetime.date(2021, 6, 15),
+				"status": "Overdue",
+				"payment_term": None,
+				"description": "_Test 50-50",
+				"due_date": datetime.date(2021, 7, 15),
+				"invoice_portion": 50.0,
+				"currency": "INR",
+				"base_payment_amount": 500000.0,
+				"paid_amount": 0.0,
+				"invoices": "",
+			},
+		]
+
+		expected_value_so2 = [
+			{
+				"name": so2.name,
+				"customer": so2.customer,
+				"submitted": datetime.date(2021, 6, 15),
+				"status": "Overdue",
+				"payment_term": None,
+				"description": "_Test 50-50",
+				"due_date": datetime.date(2021, 6, 30),
+				"invoice_portion": 50.0,
+				"currency": "INR",
+				"base_payment_amount": 50000.0,
+				"paid_amount": 0.0,
+				"invoices": "",
+			},
+			{
+				"name": so2.name,
+				"customer": so2.customer,
+				"submitted": datetime.date(2021, 6, 15),
+				"status": "Overdue",
+				"payment_term": None,
+				"description": "_Test 50-50",
+				"due_date": datetime.date(2021, 7, 15),
+				"invoice_portion": 50.0,
+				"currency": "INR",
+				"base_payment_amount": 50000.0,
+				"paid_amount": 0.0,
+				"invoices": "",
+			},
+		]
+
+		group_filters = [
+			{"customer_group": "All Customer Groups"},
+			{"item_group": "All Item Groups"},
+			{"item_group": "Products"},
+			{"item_group": "Raw Material"},
+		]
+
+		expected_values_for_group_filters = [
+			expected_value_so1 + expected_value_so2,
+			expected_value_so1 + expected_value_so2,
+			expected_value_so1,
+			expected_value_so2,
+		]
+
+		for idx, g in enumerate(group_filters, 0):
+			# build filter
+			filters = frappe._dict({}).update(base_filters).update(g)
+			with self.subTest(filters=filters):
+				columns, data, message, chart = execute(filters)
+				self.assertEqual(data, expected_values_for_group_filters[idx])
diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py
index dfcec22..4e0758d 100644
--- a/erpnext/selling/report/quotation_trends/quotation_trends.py
+++ b/erpnext/selling/report/quotation_trends/quotation_trends.py
@@ -49,7 +49,7 @@
 		"data": {
 			"labels": labels,
 			"datasets": [
-				{"name": _("{0}").format(filters.get("period")) + _(" Quoted Amount"), "values": datapoints}
+				{"name": _(filters.get("period")) + " " + _("Quoted Amount"), "values": datapoints}
 			],
 		},
 		"type": "line",
diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.py b/erpnext/selling/report/sales_order_trends/sales_order_trends.py
index 93707bd..719f1c5 100644
--- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py
+++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py
@@ -47,9 +47,7 @@
 	return {
 		"data": {
 			"labels": labels,
-			"datasets": [
-				{"name": _("{0}").format(filters.get("period")) + _(" Sales Value"), "values": datapoints}
-			],
+			"datasets": [{"name": _(filters.get("period")) + " " + _("Sales Value"), "values": datapoints}],
 		},
 		"type": "line",
 		"lineOptions": {"regionFill": 1},
diff --git a/erpnext/setup/doctype/company/company_dashboard.py b/erpnext/setup/doctype/company/company_dashboard.py
index ff1e7f1..2d073c1 100644
--- a/erpnext/setup/doctype/company/company_dashboard.py
+++ b/erpnext/setup/doctype/company/company_dashboard.py
@@ -14,7 +14,7 @@
 			"goal_doctype_link": "company",
 			"goal_field": "base_grand_total",
 			"date_field": "posting_date",
-			"filter_str": "docstatus = 1 and is_opening != 'Yes'",
+			"filters": {"docstatus": 1, "is_opening": ("!=", "Yes")},
 			"aggregation": "sum",
 		},
 		"fieldname": "company",
diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json
index 89be607..19b6ef2 100644
--- a/erpnext/setup/doctype/company/test_records.json
+++ b/erpnext/setup/doctype/company/test_records.json
@@ -8,7 +8,8 @@
 		"domain": "Manufacturing",
 		"chart_of_accounts": "Standard",
 		"default_holiday_list": "_Test Holiday List",
-		"enable_perpetual_inventory": 0
+		"enable_perpetual_inventory": 0,
+		"allow_account_creation_against_child_company": 1
 	},
 	{
 		"abbr": "_TC1",
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 6cb9f7e..6ea4525 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -54,7 +54,7 @@
 				(supplied_item.rm_item_code == self.item_code)
 				& (po.name == supplied_item.parent)
 				& (po.docstatus == 1)
-				& (po.is_subcontracted == "Yes")
+				& (po.is_subcontracted)
 				& (po.status != "Closed")
 				& (po.per_received < 100)
 				& (supplied_item.reserve_warehouse == self.warehouse)
@@ -79,7 +79,7 @@
 				& (se.name == se_item.parent)
 				& (po.name == se.purchase_order)
 				& (po.docstatus == 1)
-				& (po.is_subcontracted == "Yes")
+				& (po.is_subcontracted == 1)
 				& (po.status != "Closed")
 				& (po.per_received < 100)
 			)
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index f1f5d96..e2eb2a4 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -74,6 +74,7 @@
   "against_sales_invoice",
   "si_detail",
   "dn_detail",
+  "pick_list_item",
   "section_break_40",
   "batch_no",
   "serial_no",
@@ -762,13 +763,22 @@
    "fieldtype": "Check",
    "label": "Grant Commission",
    "read_only": 1
+  },
+  {
+   "fieldname": "pick_list_item",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Pick List Item",
+   "no_copy": 1,
+   "print_hide": 1,
+   "read_only": 1
   }
  ],
  "idx": 1,
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-02-24 14:42:20.211085",
+ "modified": "2022-03-31 18:36:24.671913",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 9e8b3bd..23301a6 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -55,10 +55,15 @@
 
 		if (frm.doc.has_variants) {
 			frm.set_intro(__("This Item is a Template and cannot be used in transactions. Item attributes will be copied over into the variants unless 'No Copy' is set"), true);
+
 			frm.add_custom_button(__("Show Variants"), function() {
 				frappe.set_route("List", "Item", {"variant_of": frm.doc.name});
 			}, __("View"));
 
+			frm.add_custom_button(__("Item Variant Settings"), function() {
+				frappe.set_route("Form", "Item Variant Settings");
+			}, __("View"));
+
 			frm.add_custom_button(__("Variant Details Report"), function() {
 				frappe.set_route("query-report", "Item Variant Details", {"item": frm.doc.name});
 			}, __("View"));
@@ -110,6 +115,13 @@
 					}
 				});
 			}, __('Actions'));
+		} else {
+			frm.add_custom_button(__("Website Item"), function() {
+				frappe.db.get_value("Website Item", {item_code: frm.doc.name}, "name", (d) => {
+					if (!d.name) frappe.throw(__("Website Item not found"));
+					frappe.set_route("Form", "Website Item", d.name);
+				});
+			}, __("View"));
 		}
 
 		erpnext.item.edit_prices_button(frm);
@@ -131,12 +143,6 @@
 			frappe.set_route('Form', 'Item', new_item.name);
 		});
 
-		if(frm.doc.has_variants) {
-			frm.add_custom_button(__("Item Variant Settings"), function() {
-				frappe.set_route("Form", "Item Variant Settings");
-			}, __("View"));
-		}
-
 		const stock_exists = (frm.doc.__onload
 			&& frm.doc.__onload.stock_exists) ? 1 : 0;
 
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 524c3d1..06da8ee 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -645,7 +645,6 @@
   },
   {
    "collapsible": 1,
-   "default": "eval:!doc.is_fixed_asset",
    "fieldname": "sales_details",
    "fieldtype": "Section Break",
    "label": "Sales Details",
@@ -992,4 +991,4 @@
  "states": [],
  "title_field": "item_name",
  "track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 9d7c22f..b2f5fb7 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -3,7 +3,7 @@
 
 import copy
 import json
-from typing import List
+from typing import Dict, List, Optional
 
 import frappe
 from frappe import _
@@ -18,6 +18,7 @@
 	now_datetime,
 	nowtime,
 	strip,
+	strip_html,
 )
 from frappe.utils.html_utils import clean_html
 
@@ -69,10 +70,6 @@
 		self.item_code = strip(self.item_code)
 		self.name = self.item_code
 
-	def before_insert(self):
-		if not self.description:
-			self.description = self.item_name
-
 	def after_insert(self):
 		"""set opening stock and item price"""
 		if self.standard_rate:
@@ -86,7 +83,7 @@
 		if not self.item_name:
 			self.item_name = self.item_code
 
-		if not self.description:
+		if not strip_html(cstr(self.description)).strip():
 			self.description = self.item_name
 
 		self.validate_uom()
@@ -464,7 +461,7 @@
 			frappe.msgprint(
 				_("It can take upto few hours for accurate stock values to be visible after merging items."),
 				indicator="orange",
-				title="Note",
+				title=_("Note"),
 			)
 
 		if self.published_in_website:
@@ -890,25 +887,38 @@
 		if self.is_new():
 			return
 
-		fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no")
+		restricted_fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no")
 
-		values = frappe.db.get_value("Item", self.name, fields, as_dict=True)
+		values = frappe.db.get_value("Item", self.name, restricted_fields, as_dict=True)
+		if not values:
+			return
+
 		if not values.get("valuation_method") and self.get("valuation_method"):
 			values["valuation_method"] = (
 				frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO"
 			)
 
-		if values:
-			for field in fields:
-				if cstr(self.get(field)) != cstr(values.get(field)):
-					if self.check_if_linked_document_exists(field):
-						frappe.throw(
-							_(
-								"As there are existing transactions against item {0}, you can not change the value of {1}"
-							).format(self.name, frappe.bold(self.meta.get_label(field)))
-						)
+		changed_fields = [
+			field for field in restricted_fields if cstr(self.get(field)) != cstr(values.get(field))
+		]
+		if not changed_fields:
+			return
 
-	def check_if_linked_document_exists(self, field):
+		if linked_doc := self._get_linked_submitted_documents(changed_fields):
+			changed_field_labels = [frappe.bold(self.meta.get_label(f)) for f in changed_fields]
+			msg = _(
+				"As there are existing submitted transactions against item {0}, you can not change the value of {1}."
+			).format(self.name, ", ".join(changed_field_labels))
+
+			if linked_doc and isinstance(linked_doc, dict):
+				msg += "<br>"
+				msg += _("Example of a linked document: {0}").format(
+					frappe.get_desk_link(linked_doc.doctype, linked_doc.docname)
+				)
+
+			frappe.throw(msg, title=_("Linked with submitted documents"))
+
+	def _get_linked_submitted_documents(self, changed_fields: List[str]) -> Optional[Dict[str, str]]:
 		linked_doctypes = [
 			"Delivery Note Item",
 			"Sales Invoice Item",
@@ -921,7 +931,7 @@
 
 		# For "Is Stock Item", following doctypes is important
 		# because reserved_qty, ordered_qty and requested_qty updated from these doctypes
-		if field == "is_stock_item":
+		if "is_stock_item" in changed_fields:
 			linked_doctypes += [
 				"Sales Order Item",
 				"Purchase Order Item",
@@ -940,11 +950,21 @@
 				"Sales Invoice Item",
 			):
 				# If Invoice has Stock impact, only then consider it.
-				if self.stock_ledger_created():
-					return True
+				if linked_doc := frappe.db.get_value(
+					"Stock Ledger Entry",
+					{"item_code": self.name, "is_cancelled": 0},
+					["voucher_no as docname", "voucher_type as doctype"],
+					as_dict=True,
+				):
+					return linked_doc
 
-			elif frappe.db.get_value(doctype, filters):
-				return True
+			elif linked_doc := frappe.db.get_value(
+				doctype,
+				filters,
+				["parent as docname", "parenttype as doctype"],
+				as_dict=True,
+			):
+				return linked_doc
 
 	def validate_auto_reorder_enabled_in_stock_settings(self):
 		if self.reorder_levels:
diff --git a/erpnext/stock/doctype/item/item_dashboard.py b/erpnext/stock/doctype/item/item_dashboard.py
index 33acf4b..897acb7 100644
--- a/erpnext/stock/doctype/item/item_dashboard.py
+++ b/erpnext/stock/doctype/item/item_dashboard.py
@@ -31,6 +31,7 @@
 			},
 			{"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]},
 			{"label": _("Traceability"), "items": ["Serial No", "Batch"]},
-			{"label": _("Move"), "items": ["Stock Entry"]},
+			{"label": _("Stock Movement"), "items": ["Stock Entry", "Stock Reconciliation"]},
+			{"label": _("E-commerce"), "items": ["Website Item"]},
 		],
 	}
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 328d937..aa0a549 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -744,6 +744,40 @@
 		self.assertTrue(get_data(warehouse="_Test Warehouse - _TC"))
 		self.assertTrue(get_data(item_group="All Item Groups"))
 
+	def test_empty_description(self):
+		item = make_item(properties={"description": "<p></p>"})
+		self.assertEqual(item.description, item.item_name)
+		item.description = ""
+		item.save()
+		self.assertEqual(item.description, item.item_name)
+
+	def test_item_type_field_change(self):
+		"""Check if critical fields like `is_stock_item`, `has_batch_no` are not changed if transactions exist."""
+		from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+		from erpnext.stock.doctype.delivery_note.test_delivery_note 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
+
+		transaction_creators = [
+			lambda i: make_purchase_receipt(item_code=i),
+			lambda i: make_purchase_invoice(item_code=i, update_stock=1),
+			lambda i: make_stock_entry(item_code=i, qty=1, target="_Test Warehouse - _TC"),
+			lambda i: create_delivery_note(item_code=i),
+		]
+
+		properties = {"has_batch_no": 0, "allow_negative_stock": 1, "valuation_rate": 10}
+		for transaction_creator in transaction_creators:
+			item = make_item(properties=properties)
+			transaction = transaction_creator(item.name)
+			item.has_batch_no = 1
+			self.assertRaises(frappe.ValidationError, item.save)
+
+			transaction.cancel()
+			# should be allowed now
+			item.reload()
+			item.has_batch_no = 1
+			item.save()
+
 
 def set_item_variant_settings(fields):
 	doc = frappe.get_doc("Item Variant Settings")
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index d829b2c..32c58c5 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -41,7 +41,7 @@
 		supplier_warehouse = "Test Supplier Warehouse - _TC"
 		po = create_purchase_order(
 			item="Test Finished Goods - A",
-			is_subcontracted="Yes",
+			is_subcontracted=1,
 			qty=5,
 			rate=3000,
 			supplier_warehouse=supplier_warehouse,
diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json
index d89ca55..eef70c9 100644
--- a/erpnext/stock/doctype/item_barcode/item_barcode.json
+++ b/erpnext/stock/doctype/item_barcode/item_barcode.json
@@ -1,109 +1,42 @@
 {
- "allow_copy": 0, 
- "allow_events_in_timeline": 0, 
- "allow_guest_to_view": 0, 
- "allow_import": 0, 
- "allow_rename": 0, 
- "autoname": "field:barcode", 
- "beta": 0, 
- "creation": "2017-12-09 18:54:50.562438", 
- "custom": 0, 
- "docstatus": 0, 
- "doctype": "DocType", 
- "document_type": "", 
- "editable_grid": 1, 
- "engine": "InnoDB", 
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2022-02-11 11:26:22.155183",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+  "barcode",
+  "barcode_type"
+ ],
  "fields": [
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "barcode", 
-   "fieldtype": "Data", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 1, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Barcode", 
-   "length": 0, 
-   "no_copy": 1, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
+   "fieldname": "barcode",
+   "fieldtype": "Data",
+   "in_global_search": 1,
+   "in_list_view": 1,
+   "label": "Barcode",
+   "no_copy": 1,
    "unique": 1
-  }, 
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "barcode_type", 
-   "fieldtype": "Select", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Barcode Type", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "\nEAN\nUPC-A", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
-   "unique": 0
+   "fieldname": "barcode_type",
+   "fieldtype": "Select",
+   "in_list_view": 1,
+   "label": "Barcode Type",
+   "options": "\nEAN\nUPC-A"
   }
- ], 
- "has_web_view": 0, 
- "hide_heading": 0, 
- "hide_toolbar": 0, 
- "idx": 0, 
- "image_view": 0, 
- "in_create": 0, 
- "is_submittable": 0, 
- "issingle": 0, 
- "istable": 1, 
- "max_attachments": 0, 
- "modified": "2018-11-13 06:03:09.814357", 
- "modified_by": "Administrator", 
- "module": "Stock", 
- "name": "Item Barcode", 
- "name_case": "", 
- "owner": "Administrator", 
- "permissions": [], 
- "quick_entry": 1, 
- "read_only": 0, 
- "read_only_onload": 0, 
- "show_name_in_global_search": 0, 
- "sort_field": "modified", 
- "sort_order": "DESC", 
- "track_changes": 1, 
- "track_seen": 0, 
- "track_views": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2022-04-01 05:54:27.314030",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Item Barcode",
+ "naming_rule": "Random",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 4524914..a70ff17 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -209,16 +209,14 @@
 						if d.ordered_qty and d.ordered_qty > allowed_qty:
 							frappe.throw(
 								_(
-									"The total Issue / Transfer quantity {0} in Material Request {1}  \
-								cannot be greater than allowed requested quantity {2} for Item {3}"
+									"The total Issue / Transfer quantity {0} in Material Request {1}  cannot be greater than allowed requested quantity {2} for Item {3}"
 								).format(d.ordered_qty, d.parent, allowed_qty, d.item_code)
 							)
 
 					elif d.ordered_qty and d.ordered_qty > d.stock_qty:
 						frappe.throw(
 							_(
-								"The total Issue / Transfer quantity {0} in Material Request {1}  \
-							cannot be greater than requested quantity {2} for Item {3}"
+								"The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}"
 							).format(d.ordered_qty, d.parent, d.qty, d.item_code)
 						)
 
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 7061ee1..33d7745 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -33,7 +33,9 @@
 				location.sales_order
 				and frappe.db.get_value("Sales Order", location.sales_order, "per_picked") == 100
 			):
-				frappe.throw("Row " + str(location.idx) + " has been picked already!")
+				frappe.throw(
+					_("Row #{}: item {} has been picked already.").format(location.idx, location.item_code)
+				)
 
 	def before_submit(self):
 		for item in self.locations:
@@ -82,10 +84,9 @@
 				100 + flt(frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance"))
 			):
 				frappe.throw(
-					"You are picking more than required quantity for "
-					+ item_code
-					+ ". Check if there is any other pick list created for "
-					+ so_doc.name
+					_(
+						"You are picking more than required quantity for {}. Check if there is any other pick list created for {}"
+					).format(item_code, so_doc.name)
 				)
 
 		frappe.db.set_value("Sales Order Item", so_item, "picked_qty", already_picked + picked_qty)
@@ -534,6 +535,7 @@
 			dn_item = map_child_doc(source_doc, delivery_note, table_mapper)
 
 			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
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 7496b6b..ec5011b 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -521,6 +521,8 @@
 			for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
 				self.assertEqual(dn_item.item_code, "_Test Item")
 				self.assertEqual(dn_item.against_sales_order, sales_order_1.name)
+				self.assertEqual(dn_item.pick_list_item, pick_list.locations[dn_item.idx - 1].name)
+
 		for dn in frappe.get_all(
 			"Delivery Note",
 			filters={"pick_list": pick_list.name, "customer": "_Test Customer 1"},
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index 0182ed5..51ec598 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -200,7 +200,7 @@
 			cur_frm.add_custom_button(__('Reopen'), this.reopen_purchase_receipt, __("Status"))
 		}
 
-		this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes");
+		this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted);
 	}
 
 	make_purchase_invoice() {
@@ -298,10 +298,10 @@
 frappe.provide("erpnext.buying");
 
 frappe.ui.form.on("Purchase Receipt", "is_subcontracted", function(frm) {
-	if (frm.doc.is_subcontracted === "Yes") {
+	if (frm.doc.is_subcontracted) {
 		erpnext.buying.get_default_bom(frm);
 	}
-	frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted==="Yes");
+	frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted);
 });
 
 frappe.ui.form.on('Purchase Receipt Item', {
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index 6d4b4a1..6e5f6f5 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -437,17 +437,16 @@
    "fieldtype": "Column Break"
   },
   {
-   "default": "No",
+   "default": "0",
    "fieldname": "is_subcontracted",
-   "fieldtype": "Select",
-   "label": "Raw Materials Consumed",
+   "fieldtype": "Check",
+   "label": "Is Subcontracted",
    "oldfieldname": "is_subcontracted",
    "oldfieldtype": "Select",
-   "options": "No\nYes",
    "print_hide": 1
   },
   {
-   "depends_on": "eval:doc.is_subcontracted==\"Yes\"",
+   "depends_on": "eval:doc.is_subcontracted",
    "fieldname": "supplier_warehouse",
    "fieldtype": "Link",
    "label": "Supplier Warehouse",
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index a6f82b0..f3faba4 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -327,7 +327,7 @@
 			target="_Test Warehouse 1 - _TC",
 			basic_rate=100,
 		)
-		pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted="Yes")
+		pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted=1)
 		self.assertEqual(len(pr.get("supplied_items")), 2)
 
 		rm_supp_cost = sum(d.amount for d in pr.get("supplied_items"))
@@ -362,7 +362,7 @@
 			item_code="_Test FG Item",
 			qty=10,
 			rate=0,
-			is_subcontracted="Yes",
+			is_subcontracted=1,
 			company="_Test Company with perpetual inventory",
 			warehouse="Stores - TCP1",
 			supplier_warehouse="Work In Progress - TCP1",
@@ -401,7 +401,7 @@
 			item_code=item_code,
 			qty=1,
 			include_exploded_items=0,
-			is_subcontracted="Yes",
+			is_subcontracted=1,
 			supplier_warehouse="_Test Warehouse 1 - _TC",
 		)
 
@@ -647,6 +647,45 @@
 		return_pr.cancel()
 		pr.cancel()
 
+	def test_purchase_receipt_for_rejected_gle_without_accepted_warehouse(self):
+		from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
+
+		rejected_warehouse = "_Test Rejected Warehouse - TCP1"
+		if not frappe.db.exists("Warehouse", rejected_warehouse):
+			get_warehouse(
+				company="_Test Company with perpetual inventory",
+				abbr=" - TCP1",
+				warehouse_name="_Test Rejected Warehouse",
+			).name
+
+		pr = make_purchase_receipt(
+			company="_Test Company with perpetual inventory",
+			warehouse="Stores - TCP1",
+			received_qty=2,
+			rejected_qty=2,
+			rejected_warehouse=rejected_warehouse,
+			do_not_save=True,
+		)
+
+		pr.items[0].qty = 0.0
+		pr.items[0].warehouse = ""
+		pr.submit()
+
+		actual_qty = frappe.db.get_value(
+			"Stock Ledger Entry",
+			{
+				"voucher_type": "Purchase Receipt",
+				"voucher_no": pr.name,
+				"warehouse": pr.items[0].rejected_warehouse,
+				"is_cancelled": 0,
+			},
+			"actual_qty",
+		)
+
+		self.assertEqual(actual_qty, 2)
+		self.assertFalse(pr.items[0].warehouse)
+		pr.cancel()
+
 	def test_purchase_return_for_serialized_items(self):
 		def _check_serial_no_values(serial_no, field_values):
 			serial_no = frappe.get_doc("Serial No", serial_no)
@@ -1122,7 +1161,7 @@
 		po = create_purchase_order(
 			item_code=item_code,
 			qty=order_qty,
-			is_subcontracted="Yes",
+			is_subcontracted=1,
 			supplier_warehouse="_Test Warehouse 1 - _TC",
 		)
 
@@ -1465,7 +1504,7 @@
 		pr.set_posting_time = 1
 	pr.company = args.company or "_Test Company"
 	pr.supplier = args.supplier or "_Test Supplier"
-	pr.is_subcontracted = args.is_subcontracted or "No"
+	pr.is_subcontracted = args.is_subcontracted or 0
 	pr.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC"
 	pr.currency = args.currency or "INR"
 	pr.is_return = args.is_return
diff --git a/erpnext/stock/doctype/purchase_receipt/test_records.json b/erpnext/stock/doctype/purchase_receipt/test_records.json
index 724e3d7..990ad12 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_records.json
+++ b/erpnext/stock/doctype/purchase_receipt/test_records.json
@@ -92,7 +92,7 @@
   "currency": "INR",
   "doctype": "Purchase Receipt",
   "base_grand_total": 5000.0,
-  "is_subcontracted": "Yes",
+  "is_subcontracted": 1,
   "base_net_total": 5000.0,
   "items": [
    {
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index e5994b2..03a4201 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -648,7 +648,7 @@
   },
   {
    "default": "0",
-   "depends_on": "eval:parent.is_subcontracted == 'Yes'",
+   "depends_on": "eval:parent.is_subcontracted",
    "fieldname": "include_exploded_items",
    "fieldtype": "Check",
    "label": "Include Exploded Items",
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index ec1d140..54c00d1 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -61,6 +61,22 @@
 
 		repost(self)
 
+	def before_cancel(self):
+		self.check_pending_repost_against_cancelled_transaction()
+
+	def check_pending_repost_against_cancelled_transaction(self):
+		if self.status not in ("Queued", "In Progress"):
+			return
+
+		if not (self.voucher_no and self.voucher_no):
+			return
+
+		transaction_status = frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus")
+		if transaction_status == 2:
+			msg = _("Cannot cancel as processing of cancelled documents is  pending.")
+			msg += "<br>" + _("Please try again in an hour.")
+			frappe.throw(msg, title=_("Pending processing"))
+
 	@frappe.whitelist()
 	def restart_reposting(self):
 		self.set_status("Queued", write=False)
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
index f3bebad..55117ce 100644
--- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -1,20 +1,25 @@
 # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
 
-import unittest
 
 import frappe
+from frappe.tests.utils import FrappeTestCase
 from frappe.utils import nowdate
 
 from erpnext.controllers.stock_controller import create_item_wise_repost_entries
+from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
 from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import (
 	in_configured_timeslot,
 )
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
 from erpnext.stock.utils import PendingRepostingError
 
 
-class TestRepostItemValuation(unittest.TestCase):
+class TestRepostItemValuation(FrappeTestCase):
+	def tearDown(self):
+		frappe.flags.dont_execute_stock_reposts = False
+
 	def test_repost_time_slot(self):
 		repost_settings = frappe.get_doc("Stock Reposting Settings")
 
@@ -162,3 +167,22 @@
 		self.assertRaises(PendingRepostingError, stock_settings.save)
 
 		riv.set_status("Skipped")
+
+	def test_prevention_of_cancelled_transaction_riv(self):
+		frappe.flags.dont_execute_stock_reposts = True
+
+		item = make_item()
+		warehouse = "_Test Warehouse - _TC"
+		old = make_stock_entry(item_code=item.name, to_warehouse=warehouse, qty=2, rate=5)
+		_new = make_stock_entry(item_code=item.name, to_warehouse=warehouse, qty=5, rate=10)
+
+		old.cancel()
+
+		riv = frappe.get_last_doc(
+			"Repost Item Valuation", {"voucher_type": old.doctype, "voucher_no": old.name}
+		)
+		self.assertRaises(frappe.ValidationError, riv.cancel)
+
+		riv.db_set("status", "Skipped")
+		riv.reload()
+		riv.cancel()  # it should cancel now
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 1aafcee..1df56ef 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -214,7 +214,7 @@
 
 		if (frm.doc.docstatus === 1) {
 			if (frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer' && frm.doc.per_transferred < 100) {
-				frm.add_custom_button('End Transit', function() {
+				frm.add_custom_button(__('End Transit'), function() {
 					frappe.model.open_mapped_doc({
 						method: "erpnext.stock.doctype.stock_entry.stock_entry.make_stock_in_entry",
 						frm: frm
@@ -633,7 +633,7 @@
 		// set allow_zero_valuation_rate to 0 if s_warehouse is selected.
 		let item = frappe.get_doc(cdt, cdn);
 		if (item.s_warehouse) {
-			item.allow_zero_valuation_rate = 0;
+			frappe.model.set_value(cdt, cdn, "allow_zero_valuation_rate", 0);
 		}
 	},
 
@@ -646,21 +646,6 @@
 		frm.events.calculate_basic_amount(frm, item);
 	},
 
-	barcode: function(doc, cdt, cdn) {
-		var d = locals[cdt][cdn];
-		if (d.barcode) {
-			frappe.call({
-				method: "erpnext.stock.get_item_details.get_item_code",
-				args: {"barcode": d.barcode },
-				callback: function(r) {
-					if (!r.exe){
-						frappe.model.set_value(cdt, cdn, "item_code", r.message);
-					}
-				}
-			});
-		}
-	},
-
 	uom: function(doc, cdt, cdn) {
 		var d = locals[cdt][cdn];
 		if(d.uom && d.item_code){
@@ -793,7 +778,7 @@
 			return {
 				"filters": {
 					"docstatus": 1,
-					"is_subcontracted": "Yes",
+					"is_subcontracted": 1,
 					"company": me.frm.doc.company
 				}
 			};
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.js b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.js
index 42cc7e6..23018aa 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.js
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.js
@@ -3,6 +3,6 @@
 
 frappe.ui.form.on('Stock Ledger Entry', {
 	refresh: function(frm) {
-
+		frm.page.btn_secondary.hide()
 	}
 });
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 5c1da42..329cd7d 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -209,6 +209,11 @@
 					msg += "<br>" + "<br>".join(authorized_users)
 					frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry"))
 
+	def on_cancel(self):
+		msg = _("Individual Stock Ledger Entry cannot be cancelled.")
+		msg += "<br>" + _("Please cancel related transaction.")
+		frappe.throw(msg)
+
 
 def on_doctype_update():
 	if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"):
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 42956a1..6561362 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
@@ -436,7 +436,7 @@
 			item_code=subcontracted_item,
 			qty=10,
 			rate=20,
-			is_subcontracted="Yes",
+			is_subcontracted=1,
 		)
 
 		self.assertEqual(pr1.items[0].valuation_rate, 120)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index 84f65a0..05dd105 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -55,6 +55,25 @@
 		}
 	},
 
+	scan_barcode: function(frm) {
+		const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:frm});
+		barcode_scanner.process_scan();
+	},
+
+	scan_mode: function(frm) {
+		if (frm.doc.scan_mode) {
+			frappe.show_alert({
+				message: __("Scan mode enabled, existing quantity will not be fetched."),
+				indicator: "green"
+			});
+		}
+	},
+
+	set_warehouse: function(frm) {
+		let transaction_controller = new erpnext.TransactionController({frm:frm});
+		transaction_controller.autofill_warehouse(frm.doc.items, "warehouse", frm.doc.set_warehouse);
+	},
+
 	get_items: function(frm) {
 		let fields = [
 			{
@@ -148,35 +167,25 @@
 					batch_no: d.batch_no
 				},
 				callback: function(r) {
-					frappe.model.set_value(cdt, cdn, "qty", r.message.qty);
+					const row = frappe.model.get_doc(cdt, cdn);
+					if (!frm.doc.scan_mode) {
+						frappe.model.set_value(cdt, cdn, "qty", r.message.qty);
+					}
 					frappe.model.set_value(cdt, cdn, "valuation_rate", r.message.rate);
 					frappe.model.set_value(cdt, cdn, "current_qty", r.message.qty);
 					frappe.model.set_value(cdt, cdn, "current_valuation_rate", r.message.rate);
 					frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty);
-					frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty);
+					frappe.model.set_value(cdt, cdn, "amount", row.qty * row.valuation_rate);
 					frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos);
 
-					if (frm.doc.purpose == "Stock Reconciliation") {
+					if (frm.doc.purpose == "Stock Reconciliation" && !frm.doc.scan_mode) {
 						frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos);
 					}
 				}
 			});
 		}
 	},
-	set_item_code: function(doc, cdt, cdn) {
-		var d = frappe.model.get_doc(cdt, cdn);
-		if (d.barcode) {
-			frappe.call({
-				method: "erpnext.stock.get_item_details.get_item_code",
-				args: {"barcode": d.barcode },
-				callback: function(r) {
-					if (!r.exe){
-						frappe.model.set_value(cdt, cdn, "item_code", r.message);
-					}
-				}
-			});
-		}
-	},
+
 	set_amount_quantity: function(doc, cdt, cdn) {
 		var d = frappe.model.get_doc(cdt, cdn);
 		if (d.qty & d.valuation_rate) {
@@ -214,13 +223,10 @@
 });
 
 frappe.ui.form.on("Stock Reconciliation Item", {
-	barcode: function(frm, cdt, cdn) {
-		frm.events.set_item_code(frm, cdt, cdn);
-	},
 
 	warehouse: function(frm, cdt, cdn) {
 		var child = locals[cdt][cdn];
-		if (child.batch_no) {
+		if (child.batch_no && !frm.doc.scan_mode) {
 			frappe.model.set_value(child.cdt, child.cdn, "batch_no", "");
 		}
 
@@ -229,7 +235,7 @@
 
 	item_code: function(frm, cdt, cdn) {
 		var child = locals[cdt][cdn];
-		if (child.batch_no) {
+		if (child.batch_no && !frm.doc.scan_mode) {
 			frappe.model.set_value(cdt, cdn, "batch_no", "");
 		}
 
@@ -255,7 +261,14 @@
 			const serial_nos = child.serial_no.trim().split('\n');
 			frappe.model.set_value(cdt, cdn, "qty", serial_nos.length);
 		}
-	}
+	},
+
+	items_add: function(frm, cdt, cdn) {
+		var item = frappe.get_doc(cdt, cdn);
+		if (!item.warehouse && frm.doc.set_warehouse) {
+			frappe.model.set_value(cdt, cdn, "warehouse", frm.doc.set_warehouse);
+		}
+	},
 
 });
 
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
index a882a61..e545b8e 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
@@ -14,6 +14,12 @@
   "posting_date",
   "posting_time",
   "set_posting_time",
+  "section_break_8",
+  "set_warehouse",
+  "section_break_22",
+  "scan_barcode",
+  "column_break_12",
+  "scan_mode",
   "sb9",
   "items",
   "section_break_9",
@@ -139,13 +145,44 @@
   {
    "fieldname": "dimension_col_break",
    "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_8",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "scan_barcode",
+   "fieldtype": "Data",
+   "label": "Scan Barcode",
+   "options": "Barcode"
+  },
+  {
+   "default": "0",
+   "description": "Disables auto-fetching of existing quantity",
+   "fieldname": "scan_mode",
+   "fieldtype": "Check",
+   "label": "Scan Mode"
+  },
+  {
+   "fieldname": "set_warehouse",
+   "fieldtype": "Link",
+   "label": "Default Warehouse",
+   "options": "Warehouse"
+  },
+  {
+   "fieldname": "section_break_22",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "column_break_12",
+   "fieldtype": "Column Break"
   }
  ],
  "icon": "fa fa-upload-alt",
  "idx": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-02-06 14:28:19.043905",
+ "modified": "2022-03-27 08:57:47.161959",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Reconciliation",
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 07a8566..5d5a27f 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -1,6 +1,7 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
+from typing import Optional
 
 import frappe
 from frappe import _, msgprint
@@ -706,29 +707,43 @@
 
 @frappe.whitelist()
 def get_stock_balance_for(
-	item_code, warehouse, posting_date, posting_time, batch_no=None, with_valuation_rate=True
+	item_code: str,
+	warehouse: str,
+	posting_date: str,
+	posting_time: str,
+	batch_no: Optional[str] = None,
+	with_valuation_rate: bool = True,
 ):
 	frappe.has_permission("Stock Reconciliation", "write", throw=True)
 
-	item_dict = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
+	item_dict = frappe.get_cached_value(
+		"Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1
+	)
 
 	if not item_dict:
 		# In cases of data upload to Items table
 		msg = _("Item {} does not exist.").format(item_code)
 		frappe.throw(msg, title=_("Missing"))
 
-	serial_nos = ""
-	with_serial_no = True if item_dict.get("has_serial_no") else False
+	serial_nos = None
+	has_serial_no = bool(item_dict.get("has_serial_no"))
+	has_batch_no = bool(item_dict.get("has_batch_no"))
+
+	if not batch_no and has_batch_no:
+		# Not enough information to fetch data
+		return {"qty": 0, "rate": 0, "serial_nos": None}
+
+	# TODO: fetch only selected batch's values
 	data = get_stock_balance(
 		item_code,
 		warehouse,
 		posting_date,
 		posting_time,
 		with_valuation_rate=with_valuation_rate,
-		with_serial_no=with_serial_no,
+		with_serial_no=has_serial_no,
 	)
 
-	if with_serial_no:
+	if has_serial_no:
 		qty, rate, serial_nos = data
 	else:
 		qty, rate = data
diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
index 6bbba05..79c2fcc 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
@@ -16,15 +16,15 @@
   "amount",
   "allow_zero_valuation_rate",
   "serial_no_and_batch_section",
-  "serial_no",
-  "column_break_11",
   "batch_no",
+  "column_break_11",
+  "serial_no",
   "section_break_3",
   "current_qty",
-  "current_serial_no",
+  "current_amount",
   "column_break_9",
   "current_valuation_rate",
-  "current_amount",
+  "current_serial_no",
   "section_break_14",
   "quantity_difference",
   "column_break_16",
@@ -181,7 +181,7 @@
  ],
  "istable": 1,
  "links": [],
- "modified": "2021-05-21 12:13:33.041266",
+ "modified": "2022-04-02 04:19:40.380587",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Reconciliation Item",
@@ -190,5 +190,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/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index 1e9d01a..5a7228a 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -38,6 +38,16 @@
 			self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse)
 			self.assertEqual(child_warehouse.is_group, 0)
 
+	def test_naming(self):
+		company = "Wind Power LLC"
+		warehouse_name = "Named Warehouse - WP"
+		wh = frappe.get_doc(doctype="Warehouse", warehouse_name=warehouse_name, company=company).insert()
+		self.assertEqual(wh.name, warehouse_name)
+
+		warehouse_name = "Unnamed Warehouse"
+		wh = frappe.get_doc(doctype="Warehouse", warehouse_name=warehouse_name, company=company).insert()
+		self.assertIn(warehouse_name, wh.name)
+
 	def test_unlinking_warehouse_from_item_defaults(self):
 		company = "_Test Company"
 
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index c892ba3..3b18a9a 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -21,8 +21,9 @@
 			suffix = " - " + frappe.get_cached_value("Company", self.company, "abbr")
 			if not self.warehouse_name.endswith(suffix):
 				self.name = self.warehouse_name + suffix
-		else:
-			self.name = self.warehouse_name
+				return
+
+		self.name = self.warehouse_name
 
 	def onload(self):
 		"""load account name for General Ledger Report"""
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index f72588e..d3a230e 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -50,7 +50,7 @@
 	        "transaction_date": None,
 	        "conversion_rate": 1.0,
 	        "buying_price_list": None,
-	        "is_subcontracted": "Yes" / "No",
+	        "is_subcontracted": 0/1,
 	        "ignore_pricing_rule": 0/1
 	        "project": ""
 	        "set_warehouse": ""
@@ -124,7 +124,7 @@
 	if args.transaction_date and item.lead_time_days:
 		out.schedule_date = out.lead_time_date = add_days(args.transaction_date, item.lead_time_days)
 
-	if args.get("is_subcontracted") == "Yes":
+	if args.get("is_subcontracted"):
 		out.bom = args.get("bom") or get_default_bom(args.item_code)
 
 	get_gross_profit(out)
@@ -167,6 +167,9 @@
 			reserved_so = get_so_reservation_for_item(args)
 			out.serial_no = get_serial_no(out, args.serial_no, sales_order=reserved_so)
 
+	if not out.serial_no:
+		out.pop("serial_no", None)
+
 
 def set_valuation_rate(out, args):
 	if frappe.db.exists("Product Bundle", args.item_code, cache=True):
@@ -237,7 +240,7 @@
 		throw(_("Item {0} is a template, please select one of its variants").format(item.name))
 
 	elif args.transaction_type == "buying" and args.doctype != "Material Request":
-		if args.get("is_subcontracted") == "Yes" and item.is_sub_contracted_item != 1:
+		if args.get("is_subcontracted") and item.is_sub_contracted_item != 1:
 			throw(_("Item {0} must be a Sub-contracted Item").format(item.name))
 
 
@@ -258,7 +261,7 @@
 	                "transaction_date": None,
 	                "conversion_rate": 1.0,
 	                "buying_price_list": None,
-	                "is_subcontracted": "Yes" / "No",
+	                "is_subcontracted": 0/1,
 	                "ignore_pricing_rule": 0/1
 	                "project": "",
 	                barcode: "",
diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
index ea27dd2..61927f5 100644
--- a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
@@ -68,7 +68,7 @@
 			options: [
 				{fieldname: 'stock_capacity', label: __('Capacity (Stock UOM)')},
 				{fieldname: 'percent_occupied', label: __('% Occupied')},
-				{fieldname: 'actual_qty', label: __('Balance Qty (Stock ')}
+				{fieldname: 'actual_qty', label: __('Balance Qty (Stock)')}
 			]
 		},
 		change: function(sort_by, sort_order) {
diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py
index a96ffef..ee151b7 100644
--- a/erpnext/stock/reorder_item.py
+++ b/erpnext/stock/reorder_item.py
@@ -246,8 +246,7 @@
 		_("Dear System Manager,")
 		+ "<br>"
 		+ _(
-			"An error occured for certain Items while creating Material Requests based on Re-order level. \
-		Please rectify these issues :"
+			"An error occured for certain Items while creating Material Requests based on Re-order level. Please rectify these issues :"
 		)
 		+ "<br>"
 	)
diff --git a/erpnext/stock/report/bom_search/bom_search.py b/erpnext/stock/report/bom_search/bom_search.py
index 3be87ab..56a65c3 100644
--- a/erpnext/stock/report/bom_search/bom_search.py
+++ b/erpnext/stock/report/bom_search/bom_search.py
@@ -3,6 +3,7 @@
 
 
 import frappe
+from frappe import _
 
 
 def execute(filters=None):
@@ -34,10 +35,10 @@
 	return [
 		{
 			"fieldname": "parent",
-			"label": "BOM",
+			"label": _("BOM"),
 			"width": 200,
 			"fieldtype": "Dynamic Link",
 			"options": "doctype",
 		},
-		{"fieldname": "doctype", "label": "Type", "width": 200, "fieldtype": "Data"},
+		{"fieldname": "doctype", "label": _("Type"), "width": 200, "fieldtype": "Data"},
 	], data
diff --git a/erpnext/stock/report/item_variant_details/item_variant_details.py b/erpnext/stock/report/item_variant_details/item_variant_details.py
index d1bf220..e3a2a65 100644
--- a/erpnext/stock/report/item_variant_details/item_variant_details.py
+++ b/erpnext/stock/report/item_variant_details/item_variant_details.py
@@ -71,7 +71,7 @@
 	columns = [
 		{
 			"fieldname": "variant_name",
-			"label": "Variant",
+			"label": _("Variant"),
 			"fieldtype": "Link",
 			"options": "Item",
 			"width": 200,
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
index 6cc9061..837c4a6 100644
--- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -4,6 +4,7 @@
 import json
 
 import frappe
+from frappe import _
 
 SLE_FIELDS = (
 	"name",
@@ -105,155 +106,155 @@
 		{
 			"fieldname": "name",
 			"fieldtype": "Link",
-			"label": "Stock Ledger Entry",
+			"label": _("Stock Ledger Entry"),
 			"options": "Stock Ledger Entry",
 		},
 		{
 			"fieldname": "posting_date",
 			"fieldtype": "Date",
-			"label": "Posting Date",
+			"label": _("Posting Date"),
 		},
 		{
 			"fieldname": "posting_time",
 			"fieldtype": "Time",
-			"label": "Posting Time",
+			"label": _("Posting Time"),
 		},
 		{
 			"fieldname": "creation",
 			"fieldtype": "Datetime",
-			"label": "Creation",
+			"label": _("Creation"),
 		},
 		{
 			"fieldname": "voucher_type",
 			"fieldtype": "Link",
-			"label": "Voucher Type",
+			"label": _("Voucher Type"),
 			"options": "DocType",
 		},
 		{
 			"fieldname": "voucher_no",
 			"fieldtype": "Dynamic Link",
-			"label": "Voucher No",
+			"label": _("Voucher No"),
 			"options": "voucher_type",
 		},
 		{
 			"fieldname": "batch_no",
 			"fieldtype": "Link",
-			"label": "Batch",
+			"label": _("Batch"),
 			"options": "Batch",
 		},
 		{
 			"fieldname": "use_batchwise_valuation",
 			"fieldtype": "Check",
-			"label": "Batchwise Valuation",
+			"label": _("Batchwise Valuation"),
 		},
 		{
 			"fieldname": "actual_qty",
 			"fieldtype": "Float",
-			"label": "Qty Change",
+			"label": _("Qty Change"),
 		},
 		{
 			"fieldname": "incoming_rate",
 			"fieldtype": "Float",
-			"label": "Incoming Rate",
+			"label": _("Incoming Rate"),
 		},
 		{
 			"fieldname": "consumption_rate",
 			"fieldtype": "Float",
-			"label": "Consumption Rate",
+			"label": _("Consumption Rate"),
 		},
 		{
 			"fieldname": "qty_after_transaction",
 			"fieldtype": "Float",
-			"label": "(A) Qty After Transaction",
+			"label": _("(A) Qty After Transaction"),
 		},
 		{
 			"fieldname": "expected_qty_after_transaction",
 			"fieldtype": "Float",
-			"label": "(B) Expected Qty After Transaction",
+			"label": _("(B) Expected Qty After Transaction"),
 		},
 		{
 			"fieldname": "difference_in_qty",
 			"fieldtype": "Float",
-			"label": "A - B",
+			"label": _("A - B"),
 		},
 		{
 			"fieldname": "stock_queue",
 			"fieldtype": "Data",
-			"label": "FIFO/LIFO Queue",
+			"label": _("FIFO/LIFO Queue"),
 		},
 		{
 			"fieldname": "fifo_queue_qty",
 			"fieldtype": "Float",
-			"label": "(C) Total qty in queue",
+			"label": _("(C) Total qty in queue"),
 		},
 		{
 			"fieldname": "fifo_qty_diff",
 			"fieldtype": "Float",
-			"label": "A - C",
+			"label": _("A - C"),
 		},
 		{
 			"fieldname": "stock_value",
 			"fieldtype": "Float",
-			"label": "(D) Balance Stock Value",
+			"label": _("(D) Balance Stock Value"),
 		},
 		{
 			"fieldname": "fifo_stock_value",
 			"fieldtype": "Float",
-			"label": "(E) Balance Stock Value in Queue",
+			"label": _("(E) Balance Stock Value in Queue"),
 		},
 		{
 			"fieldname": "fifo_value_diff",
 			"fieldtype": "Float",
-			"label": "D - E",
+			"label": _("D - E"),
 		},
 		{
 			"fieldname": "stock_value_difference",
 			"fieldtype": "Float",
-			"label": "(F) Stock Value Difference",
+			"label": _("(F) Stock Value Difference"),
 		},
 		{
 			"fieldname": "stock_value_from_diff",
 			"fieldtype": "Float",
-			"label": "Balance Stock Value using (F)",
+			"label": _("Balance Stock Value using (F)"),
 		},
 		{
 			"fieldname": "diff_value_diff",
 			"fieldtype": "Float",
-			"label": "K - D",
+			"label": _("K - D"),
 		},
 		{
 			"fieldname": "fifo_stock_diff",
 			"fieldtype": "Float",
-			"label": "(G) Stock Value difference (FIFO queue)",
+			"label": _("(G) Stock Value difference (FIFO queue)"),
 		},
 		{
 			"fieldname": "fifo_difference_diff",
 			"fieldtype": "Float",
-			"label": "F - G",
+			"label": _("F - G"),
 		},
 		{
 			"fieldname": "valuation_rate",
 			"fieldtype": "Float",
-			"label": "(H) Valuation Rate",
+			"label": _("(H) Valuation Rate"),
 		},
 		{
 			"fieldname": "fifo_valuation_rate",
 			"fieldtype": "Float",
-			"label": "(I) Valuation Rate as per FIFO",
+			"label": _("(I) Valuation Rate as per FIFO"),
 		},
 		{
 			"fieldname": "fifo_valuation_diff",
 			"fieldtype": "Float",
-			"label": "H - I",
+			"label": _("H - I"),
 		},
 		{
 			"fieldname": "balance_value_by_qty",
 			"fieldtype": "Float",
-			"label": "(J) Valuation = Value (D) ÷ Qty (A)",
+			"label": _("(J) Valuation = Value (D) ÷ Qty (A)"),
 		},
 		{
 			"fieldname": "valuation_diff",
 			"fieldtype": "Float",
-			"label": "H - J",
+			"label": _("H - J"),
 		},
 	]
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 967b2b2..b7fd65b 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -178,9 +178,9 @@
 				)
 			if repost_entry.status == "Queued":
 				doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
+				doc.status = "Skipped"
 				doc.flags.ignore_permissions = True
 				doc.cancel()
-				doc.delete()
 
 
 def set_as_cancel(voucher_type, voucher_no):
@@ -715,7 +715,7 @@
 			)
 
 		# Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
-		if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == "Yes":
+		if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
 			doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
 			doc.update_valuation_rate(reset_outgoing_rate=False)
 			for d in doc.items + doc.supplied_items:
diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py
new file mode 100644
index 0000000..9ee0c9f
--- /dev/null
+++ b/erpnext/stock/tests/test_utils.py
@@ -0,0 +1,31 @@
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.utils import scan_barcode
+
+
+class TestStockUtilities(FrappeTestCase):
+	def test_barcode_scanning(self):
+		simple_item = make_item(properties={"barcodes": [{"barcode": "12399"}]})
+		self.assertEqual(scan_barcode("12399")["item_code"], simple_item.name)
+
+		batch_item = make_item(properties={"has_batch_no": 1, "create_new_batch": 1})
+		batch = frappe.get_doc(doctype="Batch", item=batch_item.name).insert()
+
+		batch_scan = scan_barcode(batch.name)
+		self.assertEqual(batch_scan["item_code"], batch_item.name)
+		self.assertEqual(batch_scan["batch_no"], batch.name)
+		self.assertEqual(batch_scan["has_batch_no"], 1)
+		self.assertEqual(batch_scan["has_serial_no"], 0)
+
+		serial_item = make_item(properties={"has_serial_no": 1})
+		serial = frappe.get_doc(
+			doctype="Serial No", item_code=serial_item.name, serial_no=frappe.generate_hash()
+		).insert()
+
+		serial_scan = scan_barcode(serial.name)
+		self.assertEqual(serial_scan["item_code"], serial_item.name)
+		self.assertEqual(serial_scan["serial_no"], serial.name)
+		self.assertEqual(serial_scan["has_batch_no"], 0)
+		self.assertEqual(serial_scan["has_serial_no"], 1)
diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py
index 506a666..e60c1ca 100644
--- a/erpnext/stock/tests/test_valuation.py
+++ b/erpnext/stock/tests/test_valuation.py
@@ -60,9 +60,9 @@
 		self.queue.remove_stock(1, 5)
 		self.assertEqual(self.queue, [[-1, 5]])
 
-		# XXX
-		self.queue.remove_stock(1, 10)
+		self.queue.remove_stock(1)
 		self.assertTotalQty(-2)
+		self.assertEqual(self.queue, [[-2, 5]])
 
 		self.queue.add_stock(2, 10)
 		self.assertTotalQty(0)
@@ -93,7 +93,7 @@
 		self.queue.remove_stock(3, 20)
 		self.assertEqual(self.queue, [[1, 10], [5, 20]])
 
-	def test_collapsing_of_queue(self):
+	def test_queue_with_unknown_rate(self):
 		self.queue.add_stock(1, 1)
 		self.queue.add_stock(1, 2)
 		self.queue.add_stock(1, 3)
@@ -102,8 +102,7 @@
 		self.assertTotalValue(10)
 
 		self.queue.remove_stock(3, 1)
-		# XXX
-		self.assertEqual(self.queue, [[1, 7]])
+		self.assertEqual(self.queue, [[1, 4]])
 
 	def test_rounding_off(self):
 		self.queue.add_stock(1.0, 1.0)
@@ -172,6 +171,32 @@
 			self.assertTotalQty(total_qty)
 			self.assertTotalValue(total_value)
 
+	@given(stock_queue_generator, st.floats(min_value=0.1, max_value=1e6))
+	def test_fifo_qty_value_nonneg_hypothesis_with_outgoing_rate(self, stock_queue, outgoing_rate):
+		self.queue = FIFOValuation([])
+		total_qty = 0.0
+		total_value = 0.0
+
+		for qty, rate in stock_queue:
+			# don't allow negative stock
+			if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
+				continue
+			if qty > 0:
+				self.queue.add_stock(qty, rate)
+				total_qty += qty
+				total_value += qty * rate
+			else:
+				qty = abs(qty)
+				consumed = self.queue.remove_stock(qty, outgoing_rate)
+				self.assertAlmostEqual(
+					qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}"
+				)
+				total_qty -= qty
+				total_value -= sum(q * r for q, r in consumed)
+			self.assertTotalQty(total_qty)
+			self.assertTotalValue(total_value)
+			self.assertGreaterEqual(total_value, 0)
+
 
 class TestLIFOValuation(unittest.TestCase):
 	def setUp(self):
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 4f1891f..d40218e 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -3,6 +3,7 @@
 
 
 import json
+from typing import Dict, Optional
 
 import frappe
 from frappe import _
@@ -548,3 +549,51 @@
 		)
 
 	return bool(reposting_pending)
+
+
+@frappe.whitelist()
+def scan_barcode(search_value: str) -> Dict[str, Optional[str]]:
+
+	# search barcode no
+	barcode_data = frappe.db.get_value(
+		"Item Barcode",
+		{"barcode": search_value},
+		["barcode", "parent as item_code"],
+		as_dict=True,
+	)
+	if barcode_data:
+		return _update_item_info(barcode_data)
+
+	# search serial no
+	serial_no_data = frappe.db.get_value(
+		"Serial No",
+		search_value,
+		["name as serial_no", "item_code", "batch_no"],
+		as_dict=True,
+	)
+	if serial_no_data:
+		return _update_item_info(serial_no_data)
+
+	# search batch no
+	batch_no_data = frappe.db.get_value(
+		"Batch",
+		search_value,
+		["name as batch_no", "item as item_code"],
+		as_dict=True,
+	)
+	if batch_no_data:
+		return _update_item_info(batch_no_data)
+
+	return {}
+
+
+def _update_item_info(scan_result: Dict[str, Optional[str]]) -> Dict[str, Optional[str]]:
+	if item_code := scan_result.get("item_code"):
+		if item_info := frappe.get_cached_value(
+			"Item",
+			item_code,
+			["has_batch_no", "has_serial_no"],
+			as_dict=True,
+		):
+			scan_result.update(item_info)
+	return scan_result
diff --git a/erpnext/stock/valuation.py b/erpnext/stock/valuation.py
index 648b218..35f4f12 100644
--- a/erpnext/stock/valuation.py
+++ b/erpnext/stock/valuation.py
@@ -60,9 +60,7 @@
 
 	# specifying the attributes to save resources
 	# ref: https://docs.python.org/3/reference/datamodel.html#slots
-	__slots__ = [
-		"queue",
-	]
+	__slots__ = ["queue"]
 
 	def __init__(self, state: Optional[List[StockBin]]):
 		self.queue: List[StockBin] = state if state is not None else []
@@ -123,15 +121,9 @@
 						index = idx
 						break
 
-				# If no entry found with outgoing rate, collapse queue
+				# If no entry found with outgoing rate, consume as per FIFO
 				if index is None:  # nosemgrep
-					new_stock_value = sum(d[QTY] * d[RATE] for d in self.queue) - qty * outgoing_rate
-					new_stock_qty = sum(d[QTY] for d in self.queue) - qty
-					self.queue = [
-						[new_stock_qty, new_stock_value / new_stock_qty if new_stock_qty > 0 else outgoing_rate]
-					]
-					consumed_bins.append([qty, outgoing_rate])
-					break
+					index = 0
 			else:
 				index = 0
 
@@ -172,9 +164,7 @@
 
 	# specifying the attributes to save resources
 	# ref: https://docs.python.org/3/reference/datamodel.html#slots
-	__slots__ = [
-		"stack",
-	]
+	__slots__ = ["stack"]
 
 	def __init__(self, state: Optional[List[StockBin]]):
 		self.stack: List[StockBin] = state if state is not None else []
diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py
index 5b51ef8..57fa7bf 100644
--- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py
+++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py
@@ -3,15 +3,16 @@
 
 
 import frappe
+from frappe import _
 
 
 def execute(filters=None):
 	columns = [
-		{"fieldname": "creation_date", "label": "Date", "fieldtype": "Date", "width": 300},
+		{"fieldname": "creation_date", "label": _("Date"), "fieldtype": "Date", "width": 300},
 		{
 			"fieldname": "first_response_time",
 			"fieldtype": "Duration",
-			"label": "First Response Time",
+			"label": _("First Response Time"),
 			"width": 300,
 		},
 	]
diff --git a/erpnext/templates/includes/projects/project_row.html b/erpnext/templates/includes/projects/project_row.html
index a256fbd..686637a 100644
--- a/erpnext/templates/includes/projects/project_row.html
+++ b/erpnext/templates/includes/projects/project_row.html
@@ -1,11 +1,11 @@
 {% if doc.status == "Open" %}
   <div class="web-list-item transaction-list-item">
     <div class="row">
-      <div class="col-xs-2">
+      <div class="col-xs-2 project-link">
         <a class="transaction-item-link" href="/projects?project={{ doc.name | urlencode }}">Link</a>
         {{ doc.name }}
       </div>
-      <div class="col-xs-2">
+      <div class="col-xs-2 project-name">
         {{ doc.project_name }}
       </div>
       <div class="col-xs-3 text-center">
@@ -25,7 +25,7 @@
       </div>
       {% if doc["_assign"] %}
         {% set assigned_users = json.loads(doc["_assign"])%}
-        <div class="col-xs-2">
+        <div class="col-xs-2 project-users">
           {% for user in assigned_users %}
             {% set user_details = frappe
               .db
@@ -46,7 +46,7 @@
           {% endfor %}
         </div>
       {% endif %}
-      <div class="col-xs-3 text-right small text-muted">
+      <div class="col-xs-3 text-right small text-muted project-modified-on">
         {{ frappe.utils.pretty_date(doc.modified) }}
       </div>
     </div>
diff --git a/erpnext/templates/pages/courses.html b/erpnext/templates/pages/courses.html
deleted file mode 100644
index 6592f7a..0000000
--- a/erpnext/templates/pages/courses.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends "templates/web.html" %}
-
-{% block header %}
-	<h1> About </h1>
-{% endblock %}
-
-{% block page_content %}
-
-<p class="post-description"> {{ intro }} </p>
-
-{% endblock %}
diff --git a/erpnext/templates/pages/courses.py b/erpnext/templates/pages/courses.py
deleted file mode 100644
index fb1af38..0000000
--- a/erpnext/templates/pages/courses.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-
-
-def get_context(context):
-	course = frappe.get_doc("Course", frappe.form_dict.course)
-	sidebar_title = course.name
-
-	context.no_cache = 1
-	context.show_sidebar = True
-	course = frappe.get_doc("Course", frappe.form_dict.course)
-	course.has_permission("read")
-	context.doc = course
-	context.sidebar_title = sidebar_title
-	context.intro = course.course_intro
diff --git a/erpnext/templates/pages/home.py b/erpnext/templates/pages/home.py
index bca3e56..47fb89d 100644
--- a/erpnext/templates/pages/home.py
+++ b/erpnext/templates/pages/home.py
@@ -8,7 +8,7 @@
 
 
 def get_context(context):
-	homepage = frappe.get_doc("Homepage")
+	homepage = frappe.get_cached_doc("Homepage")
 
 	for item in homepage.products:
 		route = frappe.db.get_value("Website Item", {"item_code": item.item_code}, "route")
@@ -20,10 +20,10 @@
 	context.homepage = homepage
 
 	if homepage.hero_section_based_on == "Homepage Section" and homepage.hero_section:
-		homepage.hero_section_doc = frappe.get_doc("Homepage Section", homepage.hero_section)
+		homepage.hero_section_doc = frappe.get_cached_doc("Homepage Section", homepage.hero_section)
 
 	if homepage.slideshow:
-		doc = frappe.get_doc("Website Slideshow", homepage.slideshow)
+		doc = frappe.get_cached_doc("Website Slideshow", homepage.slideshow)
 		context.slideshow = homepage.slideshow
 		context.slideshow_header = doc.header
 		context.slides = doc.slideshow_items
@@ -46,7 +46,7 @@
 		order_by="section_order asc",
 	)
 	context.homepage_sections = [
-		frappe.get_doc("Homepage Section", name) for name in homepage_sections
+		frappe.get_cached_doc("Homepage Section", name) for name in homepage_sections
 	]
 
 	context.metatags = context.metatags or frappe._dict({})
diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py
index 77a749e..3ed056f 100644
--- a/erpnext/templates/pages/product_search.py
+++ b/erpnext/templates/pages/product_search.py
@@ -1,6 +1,8 @@
 # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
+import json
+
 import frappe
 from frappe.utils import cint, cstr
 from redisearch import AutoCompleter, Client, Query
@@ -9,7 +11,7 @@
 	WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
 	WEBSITE_ITEM_INDEX,
 	WEBSITE_ITEM_NAME_AUTOCOMPLETE,
-	is_search_module_loaded,
+	is_redisearch_enabled,
 	make_key,
 )
 from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website
@@ -74,8 +76,8 @@
 def product_search(query, limit=10, fuzzy_search=True):
 	search_results = {"from_redisearch": True, "results": []}
 
-	if not is_search_module_loaded():
-		# Redisearch module not loaded
+	if not is_redisearch_enabled():
+		# Redisearch module not enabled
 		search_results["from_redisearch"] = False
 		search_results["results"] = get_product_data(query, 0, limit)
 		return search_results
@@ -86,6 +88,8 @@
 	red = frappe.cache()
 	query = clean_up_query(query)
 
+	# TODO: Check perf/correctness with Suggestions & Query vs only Query
+	# TODO: Use Levenshtein Distance in Query (max=3)
 	ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red)
 	client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red)
 	suggestions = ac.get_suggestions(
@@ -121,8 +125,8 @@
 def get_category_suggestions(query):
 	search_results = {"results": []}
 
-	if not is_search_module_loaded():
-		# Redisearch module not loaded, query db
+	if not is_redisearch_enabled():
+		# Redisearch module not enabled, query db
 		categories = frappe.db.get_all(
 			"Item Group",
 			filters={"name": ["like", "%{0}%".format(query)], "show_in_website": 1},
@@ -135,8 +139,10 @@
 		return search_results
 
 	ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache())
-	suggestions = ac.get_suggestions(query, num=10)
+	suggestions = ac.get_suggestions(query, num=10, with_payloads=True)
 
-	search_results["results"] = [s.string for s in suggestions]
+	results = [json.loads(s.payload) for s in suggestions]
+
+	search_results["results"] = results
 
 	return search_results
diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py
index 07291e8..bf12181 100644
--- a/erpnext/tests/test_subcontracting.py
+++ b/erpnext/tests/test_subcontracting.py
@@ -50,7 +50,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
@@ -112,7 +112,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
@@ -175,7 +175,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
@@ -239,7 +239,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
@@ -298,7 +298,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
@@ -363,7 +363,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
@@ -421,7 +421,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
@@ -492,7 +492,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
@@ -529,7 +529,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
@@ -609,7 +609,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
@@ -675,7 +675,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
@@ -751,7 +751,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
@@ -834,7 +834,7 @@
 
 		itemwise_details = make_stock_in_entry(rm_items=rm_items)
 		po = create_purchase_order(
-			rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
+			rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
 		)
 
 		for d in rm_items:
diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv
index 3cdae45..db454de 100644
--- a/erpnext/translations/fr.csv
+++ b/erpnext/translations/fr.csv
@@ -285,7 +285,7 @@
 "Asset {0} cannot be scrapped, as it is already {1}","L'actif {0} ne peut pas être mis au rebut, car il est déjà {1}",
 Asset {0} does not belong to company {1},L'actif {0} ne fait pas partie à la société {1},
 Asset {0} must be submitted,L'actif {0} doit être soumis,
-Assets,Les atouts,
+Assets,Actifs - Immo.,
 Assign,Assigner,
 Assign Salary Structure,Affecter la structure salariale,
 Assign To,Attribuer À,
@@ -1211,7 +1211,7 @@
 Help Results for,Aide Résultats pour,
 High,Haut,
 High Sensitivity,Haute sensibilité,
-Hold,Tenir,
+Hold,Mettre en attente,
 Hold Invoice,Facture en attente,
 Holiday,Vacances,
 Holiday List,Liste de vacances,
@@ -4240,7 +4240,7 @@
 From date cannot be greater than To date,La Date Initiale ne peut pas être postérieure à la Date Finale,
 Group by,Grouper Par,
 In stock,En stock,
-Item name,Nom de l'article,
+Item name,Libellé de l'article,
 Loan amount is mandatory,Le montant du prêt est obligatoire,
 Minimum Qty,Quantité minimum,
 More details,Plus de détails,
@@ -5473,7 +5473,7 @@
 PUR-ORD-.YYYY.-,PUR-ORD-.YYYY.-,
 Get Items from Open Material Requests,Obtenir des Articles de Demandes Matérielles Ouvertes,
 Fetch items based on Default Supplier.,Récupérez les articles en fonction du fournisseur par défaut.,
-Required By,Requis Par,
+Required By,Requis pour le,
 Order Confirmation No,No de confirmation de commande,
 Order Confirmation Date,Date de confirmation de la commande,
 Customer Mobile No,N° de Portable du Client,
@@ -7223,8 +7223,8 @@
 Scrap %,% de Rebut,
 Original Item,Article original,
 BOM Operation,Opération LDM,
-Operation Time ,Moment de l&#39;opération,
-In minutes,En quelques minutes,
+Operation Time ,Durée de l&#39;opération,
+In minutes,En minutes,
 Batch Size,Taille du lot,
 Base Hour Rate(Company Currency),Taux Horaire de Base (Devise de la Société),
 Operating Cost(Company Currency),Coût d'Exploitation (Devise Société),
@@ -9267,7 +9267,7 @@
 Amount Delivered,Montant livré,
 Delay (in Days),Retard (en jours),
 Group by Sales Order,Regrouper par commande client,
- Sales Value,La valeur des ventes,
+Sales Value,La valeur des ventes,
 Stock Qty vs Serial No Count,Quantité de stock vs numéro de série,
 Serial No Count,Numéro de série,
 Work Order Summary,Résumé de l&#39;ordre de travail,
@@ -9647,7 +9647,7 @@
 Validate Selling Price for Item Against Purchase Rate or Valuation Rate,Valider le prix de vente de l&#39;article par rapport au taux d&#39;achat ou au taux de valorisation,
 Hide Customer's Tax ID from Sales Transactions,Masquer le numéro d&#39;identification fiscale du client dans les transactions de vente,
 "The percentage you are allowed to receive or deliver more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed to receive 110 units.","Le pourcentage que vous êtes autorisé à recevoir ou à livrer plus par rapport à la quantité commandée. Par exemple, si vous avez commandé 100 unités et que votre allocation est de 10%, vous êtes autorisé à recevoir 110 unités.",
-Action If Quality Inspection Is Not Submitted,Action si l&#39;inspection de la qualité n&#39;est pas soumise,
+Action If Quality Inspection Is Not Submitted,Action si l&#39;inspection qualité n&#39;est pas soumise,
 Auto Insert Price List Rate If Missing,Taux de liste de prix d&#39;insertion automatique s&#39;il est manquant,
 Automatically Set Serial Nos Based on FIFO,Définir automatiquement les numéros de série en fonction de FIFO,
 Set Qty in Transactions Based on Serial No Input,Définir la quantité dans les transactions en fonction du numéro de série,
@@ -9838,3 +9838,35 @@
 Creating Purchase Order ...,Création d&#39;une commande d&#39;achat ...,
 "Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Sélectionnez un fournisseur parmi les fournisseurs par défaut des articles ci-dessous. Lors de la sélection, un bon de commande sera effectué contre des articles appartenant uniquement au fournisseur sélectionné.",
 Row #{}: You must select {} serial numbers for item {}.,Ligne n ° {}: vous devez sélectionner {} numéros de série pour l&#39;article {}.,
+Update Rate as per Last Purchase,Mettre à jour avec les derniers prix d'achats
+Company Shipping Address,Adresse d&#39;expédition
+Shipping Address Details,Détail d&#39;adresse d&#39;expédition
+Company Billing Address,Adresse de la société de facturation
+Supplier Address Details,
+Bank Reconciliation Tool,Outil de réconcialiation d&#39;écritures bancaires
+Supplier Contact,Contact fournisseur
+Subcontracting,Sous traitance
+Order Status,Statut de la commande
+Build,Personnalisations avancées
+Dispatch Address Name,Adresse de livraison intermédiaire
+Amount Eligible for Commission,Montant éligible à comission
+Grant Commission,Eligible aux commissions
+Stock Transactions Settings, Paramétre des transactions
+Role Allowed to Over Deliver/Receive, Rôle autorisé à dépasser cette limite
+Users with this role are allowed to over deliver/receive against orders above the allowance percentage,Rôle Utilisateur qui sont autorisé à livrée/commandé au-delà de la limite
+Over Transfer Allowance,Autorisation de limite de transfert
+Quality Inspection Settings,Paramétre de l&#39;inspection qualité
+Action If Quality Inspection Is Rejected,Action si l'inspection qualité est rejetée
+Disable Serial No And Batch Selector,Désactiver le sélecteur de numéro de lot/série
+Is Rate Adjustment Entry (Debit Note),Est un justement du prix de la note de débit
+Issue a debit note with 0 qty against an existing Sales Invoice,Creer une note de débit avec une quatité à O pour la facture
+Control Historical Stock Transactions,Controle de l&#39;historique des stransaction de stock
+No stock transactions can be created or modified before this date.,Aucune transaction ne peux être créée ou modifié avant cette date.
+Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées
+Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée
+"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","LEs utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire"
+Auto Insert Item Price If Missing,Création du prix de l'article dans les listes de prix si abscent
+Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix
+Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock
+Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions
+Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries
diff --git a/erpnext/utilities/doctype/video/video.js b/erpnext/utilities/doctype/video/video.js
index 9cb5a15..e6c6efb 100644
--- a/erpnext/utilities/doctype/video/video.js
+++ b/erpnext/utilities/doctype/video/video.js
@@ -4,7 +4,7 @@
 frappe.ui.form.on('Video', {
 	refresh: function (frm) {
 		frm.events.toggle_youtube_statistics_section(frm);
-		frm.add_custom_button("Watch Video", () => frappe.help.show_video(frm.doc.url, frm.doc.title));
+		frm.add_custom_button(__("Watch Video"), () => frappe.help.show_video(frm.doc.url, frm.doc.title));
 	},
 
 	toggle_youtube_statistics_section: (frm) => {
diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py
index a65a75f..a2cb4e8 100644
--- a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py
+++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py
@@ -67,7 +67,7 @@
 		{
 			"value": total_views,
 			"indicator": "Blue",
-			"label": "Total Views",
+			"label": _("Total Views"),
 			"datatype": "Float",
 		}
 	]
diff --git a/requirements.txt b/requirements.txt
index 39591ca..657054f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
 # frappe   # https://github.com/frappe/frappe is installed during bench-init
 gocardless-pro~=1.22.0
 googlemaps
-pandas~=1.1.5
+pandas>=1.1.5,<2.0.0
 plaid-python~=7.2.1
 pycountry~=20.7.3
 PyGithub~=1.55
@@ -10,4 +10,4 @@
 taxjar~=1.9.2
 tweepy~=3.10.0
 Unidecode~=1.2.0
-redisearch==2.0.0
\ No newline at end of file
+redisearch~=2.1.0