Merge branch 'develop' into currency-exchange-settings
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index c145291..0000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-name: Bug report
-about: Report a bug encountered while using ERPNext
-labels: bug
----
-
-<!--
-Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
-
-1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
-    - For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
-    - For documentation issues, refer to https://github.com/frappe/erpnext_com
-2. Use the search function before creating a new issue. Duplicates will be closed and directed to
-   the original discussion.
-3. When making a bug report, make sure you provide all required information. The easier it is for
-   maintainers to reproduce, the faster it'll be fixed.
-4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
--->
-
-## Description of the issue
-
-## Context information (for bug reports)
-
-**Output of `bench version`**
-```
-(paste here)
-```
-
-## Steps to reproduce the issue
-
-1.
-2.
-3.
-
-### Observed result
-
-### Expected result
-
-### Stacktrace / full error message
-
-```
-(paste here)
-```
-
-## Additional information
-
-OS version / distribution, `ERPNext` install method, etc.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
new file mode 100644
index 0000000..a6e16a0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -0,0 +1,106 @@
+name: Bug Report
+description: Report a bug encountered while using ERPNext
+labels: ["bug"]
+
+body:
+  - type: markdown
+    attributes:
+      value: |
+        Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
+
+        1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
+            - For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
+            - For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
+        2. When making a bug report, make sure you provide all required information. The easier it is for
+           maintainers to reproduce, the faster it'll be fixed.
+        3. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
+
+  - type: textarea
+    id: bug-info
+    attributes:
+      label: Information about bug
+      description: Also tell us, what did you expect to happen?
+      placeholder: Please provide as much information as possible.
+    validations:
+      required: true
+
+  - type: dropdown
+    id: version
+    attributes:
+      label: Version
+      description: Affected versions.
+      multiple: true
+      options:
+        - v12
+        - v13
+        - v14
+        - develop
+    validations:
+      required: true
+
+  - type: dropdown
+    id: module
+    attributes:
+      label: Module
+      description: Select affected module of ERPNext.
+      multiple: true
+      options:
+        - accounts
+        - stock
+        - buying
+        - selling
+        - ecommerce
+        - manufacturing
+        - HR
+        - projects
+        - support
+        - assets
+        - integrations
+        - quality
+        - regional
+        - portal
+        - agriculture
+        - education
+        - non-profit
+    validations:
+      required: true
+
+  - type: textarea
+    id: exact-version
+    attributes:
+      label: Version
+      description: Share exact version number of Frappe and ERPNext you are using.
+      placeholder: |
+        Frappe version -
+        ERPNext Verion -
+    validations:
+      required: true
+
+  - type: dropdown
+    id: install-method
+    attributes:
+      label: Installation method
+      options:
+        - docker
+        - easy-install
+        - manual install
+        - FrappeCloud
+    validations:
+      required: true
+
+  - type: textarea
+    id: logs
+    attributes:
+      label: Relevant log output / Stack trace / Full Error Message.
+      description: Please copy and paste any relevant log output. This will be automatically formatted.
+      render: shell
+
+
+  - type: checkboxes
+    id: terms
+    attributes:
+      label: Code of Conduct
+      description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md)
+      options:
+        - label: I agree to follow this project's Code of Conduct
+          required: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 6cdad35..418bf3c 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,7 +1,10 @@
 ---
 name: Feature request
 about: Suggest an idea to improve ERPNext
+title: ''
 labels: feature-request
+assignees: ''
+
 ---
 
 <!--
diff --git a/.github/ISSUE_TEMPLATE/question-about-using-erpnext.md b/.github/ISSUE_TEMPLATE/question-about-using-erpnext.md
deleted file mode 100644
index 2016bcc..0000000
--- a/.github/ISSUE_TEMPLATE/question-about-using-erpnext.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-name: Question about using ERPNext
-about: This is not the appropriate channel
-labels: invalid
----
-
-Please post on our forums:
-
-for questions about using `ERPNext`: https://discuss.erpnext.com
-
-for questions about using the `Frappe Framework`: ~~https://discuss.frappe.io~~ => [stackoverflow](https://stackoverflow.com/questions/tagged/frappe) tagged under `frappe`
-
-for questions about using `bench`, probably the best place to start is the [bench repo](https://github.com/frappe/bench)
-
-For documentation issues, use the [ERPNext Documentation](https://erpnext.com/docs/) or [Frappe Framework Documentation](https://frappe.io/docs/user/en) or the [developer cheetsheet](https://github.com/frappe/frappe/wiki/Developer-Cheatsheet)
-
-> **Posts that are not bug reports or feature requests will not be addressed on this issue tracker.**
\ No newline at end of file
diff --git a/.github/stale.yml b/.github/stale.yml
index 9322ae8..8b7cb9b 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -1,34 +1,36 @@
 # Configuration for probot-stale - https://github.com/probot/stale
 
-# Number of days of inactivity before an Issue or Pull Request becomes stale
-daysUntilStale: 15
+# Label to use when marking as stale
+staleLabel: inactive
 
-# Number of days of inactivity before a stale Issue or Pull Request is closed.
-# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
-daysUntilClose: 3
-
-# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
-exemptLabels:
-  - hotfix
+# Limit the number of actions per hour, from 1-30. Default is 30
+limitPerRun: 10
 
 # Set to true to ignore issues in a project (defaults to false)
-exemptProjects: false
+exemptProjects: true
 
 # Set to true to ignore issues in a milestone (defaults to false)
 exemptMilestones: true
 
-# Label to use when marking as stale
-staleLabel: inactive
+pulls:
+  daysUntilStale: 15
+  daysUntilClose: 3
+  exemptLabels:
+    - hotfix
+  markComment: >
+    This pull request has been automatically marked as inactive because it has
+    not had recent activity. It will be closed within 3 days if no further
+    activity occurs, but it only takes a comment to keep a contribution alive
+    :) Also, even if it is closed, you can always reopen the PR when you're
+    ready. Thank you for contributing.
 
-# Comment to post when marking as stale. Set to `false` to disable
-markComment: >
-  This pull request has been automatically marked as stale because it has not had
-  recent activity. It will be closed within a week if no further activity occurs, but it
-  only takes a comment to keep a contribution alive :) Also, even if it is closed,
-  you can always reopen the PR when you're ready. Thank you for contributing.
-  
-# Limit the number of actions per hour, from 1-30. Default is 30
-limitPerRun: 30
-
-# Limit to only `issues` or `pulls`
-only: pulls
+issues:
+  daysUntilStale: 60
+  daysUntilClose: 7
+  exemptLabels:
+    - valid
+    - to-validate
+  markComment: >
+    This issue has been automatically marked as inactive because it has not had
+    recent activity and it wasn't validated by maintainer team. It will be
+    closed within a week if no further activity occurs.
diff --git a/codecov.yml b/codecov.yml
index 67bd445..1fa602a 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -8,6 +8,16 @@
         target: auto
         threshold: 0.5%
 
+    patch:
+      default:
+        target: 85%
+        threshold: 0%
+        base: auto
+        branches:
+          - develop
+        if_ci_failed: ignore
+        only_pulls: true
+
 comment:
   layout: "diff, files"
   require_changes: true
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index 7451917..4839207 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -10,6 +10,8 @@
 from frappe.model.document import Document
 from frappe.utils import cint
 
+from erpnext.stock.utils import check_pending_reposting
+
 
 class AccountsSettings(Document):
 	def on_update(self):
@@ -25,6 +27,7 @@
 		self.validate_stale_days()
 		self.enable_payment_schedule_in_print()
 		self.toggle_discount_accounting_fields()
+		self.validate_pending_reposts()
 
 	def validate_stale_days(self):
 		if not self.allow_stale and cint(self.stale_days) <= 0:
@@ -56,3 +59,8 @@
 				make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
 
 		make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
+
+
+	def validate_pending_reposts(self):
+		if self.acc_frozen_upto:
+			check_pending_reposting(self.acc_frozen_upto)
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index 2a923f0..ddb833f 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -159,7 +159,8 @@
 			frappe.scrub(row.party_type): row.party,
 			"is_pos": 0,
 			"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
-			"update_stock": 0
+			"update_stock": 0,
+			"invoice_number": row.invoice_number
 		})
 
 		accounting_dimension = get_accounting_dimensions()
@@ -200,10 +201,13 @@
 	names = []
 	for idx, d in enumerate(invoices):
 		try:
+			invoice_number = None
+			if d.invoice_number:
+				invoice_number = d.invoice_number
 			publish(idx, len(invoices), d.doctype)
 			doc = frappe.get_doc(d)
 			doc.flags.ignore_mandatory = True
-			doc.insert()
+			doc.insert(set_name=invoice_number)
 			doc.submit()
 			frappe.db.commit()
 			names.append(doc.name)
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
index c795e83..07c72bd 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
@@ -18,10 +18,10 @@
 		if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
 			make_company()
 
-	def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
+	def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None):
 		doc = frappe.get_single("Opening Invoice Creation Tool")
 		args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
-			party_1=party_1, party_2=party_2)
+			party_1=party_1, party_2=party_2, invoice_number=invoice_number)
 		doc.update(args)
 		return doc.make_invoices()
 
@@ -92,6 +92,20 @@
 		# teardown
 		frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
 
+	def test_renaming_of_invoice_using_invoice_number_field(self):
+		company = "_Test Opening Invoice Company"
+		party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
+		self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11")
+
+		sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name")
+		sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name")
+		self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
+
+		#teardown
+		for inv in [sales_inv1, sales_inv2]:
+			doc = frappe.get_doc('Sales Invoice', inv)
+			doc.cancel()
+
 def get_opening_invoice_creation_dict(**args):
 	party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
 	company = args.get("company", "_Test Company")
@@ -107,7 +121,8 @@
 				"item_name": "Opening Item",
 				"due_date": "2016-09-10",
 				"posting_date": "2016-09-05",
-				"temporary_opening_account": get_temporary_opening_account(company)
+				"temporary_opening_account": get_temporary_opening_account(company),
+				"invoice_number": args.get("invoice_number")
 			},
 			{
 				"qty": 2.0,
@@ -116,7 +131,8 @@
 				"item_name": "Opening Item",
 				"due_date": "2016-09-10",
 				"posting_date": "2016-09-05",
-				"temporary_opening_account": get_temporary_opening_account(company)
+				"temporary_opening_account": get_temporary_opening_account(company),
+				"invoice_number": None
 			}
 		]
 	})
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json
index 4ce8cb9..5c19091 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json
@@ -1,9 +1,11 @@
 {
+ "actions": [],
  "creation": "2017-08-29 04:26:36.159247",
  "doctype": "DocType",
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
+  "invoice_number",
   "party_type",
   "party",
   "temporary_opening_account",
@@ -103,10 +105,18 @@
   {
    "fieldname": "dimension_col_break",
    "fieldtype": "Column Break"
+  },
+  {
+   "description": "Reference number of the invoice from the previous system",
+   "fieldname": "invoice_number",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Invoice Number"
   }
  ],
  "istable": 1,
- "modified": "2019-07-25 15:00:00.460695",
+ "links": [],
+ "modified": "2021-12-17 19:25:06.053187",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Opening Invoice Creation Tool Item",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 48b5cb9..df957d2 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -114,6 +114,9 @@
 		self.set_status()
 		self.validate_purchase_receipt_if_update_stock()
 		validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
+		self.reset_default_field_value("set_warehouse", "items", "warehouse")
+		self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
+		self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
 
 	def validate_release_date(self):
 		if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
@@ -294,8 +297,15 @@
 						item.expense_account = stock_not_billed_account
 
 			elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
-				item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
+				asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
 					company = self.company)
+				if not asset_category_account:
+					form_link = get_link_to_form('Asset Category', asset_category)
+					throw(
+						_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
+						title=_("Missing Account")
+					)
+				item.expense_account = asset_category_account
 			elif item.is_fixed_asset and item.pr_detail:
 				item.expense_account = asset_received_but_not_billed
 			elif not item.expense_account and for_validate:
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index c4d59f1..64712b5 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -155,6 +155,8 @@
 		if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated:
 			validate_loyalty_points(self, self.loyalty_points)
 
+		self.reset_default_field_value("set_warehouse", "items", "warehouse")
+
 	def validate_fixed_asset(self):
 		for d in self.get("items"):
 			if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 1dae87f..467d4a1 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -23,6 +23,7 @@
 	get_accounting_dimensions,
 )
 from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
+from erpnext.accounts.party import get_party_account_currency
 
 
 class Subscription(Document):
@@ -355,6 +356,9 @@
 			if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'):
 				invoice.apply_tds = 1
 
+		### Add party currency to invoice
+		invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
+
 		## Add dimensions in invoice for subscription:
 		accounting_dimensions = get_accounting_dimensions()
 
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index 9dd370b..6f67bc5 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -60,15 +60,38 @@
 		plan.billing_interval_count = 3
 		plan.insert()
 
+	if not frappe.db.exists('Subscription Plan', '_Test Plan Multicurrency'):
+		plan = frappe.new_doc('Subscription Plan')
+		plan.plan_name = '_Test Plan Multicurrency'
+		plan.item = '_Test Non Stock Item'
+		plan.price_determination = "Fixed Rate"
+		plan.cost = 50
+		plan.currency = 'USD'
+		plan.billing_interval = 'Month'
+		plan.billing_interval_count = 1
+		plan.insert()
+
+def create_parties():
 	if not frappe.db.exists('Supplier', '_Test Supplier'):
 		supplier = frappe.new_doc('Supplier')
 		supplier.supplier_name = '_Test Supplier'
 		supplier.supplier_group = 'All Supplier Groups'
 		supplier.insert()
 
+	if not frappe.db.exists('Customer', '_Test Subscription Customer'):
+		customer = frappe.new_doc('Customer')
+		customer.customer_name = '_Test Subscription Customer'
+		customer.billing_currency = 'USD'
+		customer.append('accounts', {
+			'company': '_Test Company',
+			'account': '_Test Receivable USD - _TC'
+		})
+		customer.insert()
+
 class TestSubscription(unittest.TestCase):
 	def setUp(self):
 		create_plan()
+		create_parties()
 
 	def test_create_subscription_with_trial_with_correct_period(self):
 		subscription = frappe.new_doc('Subscription')
@@ -637,3 +660,22 @@
 
 		subscription.process()
 		self.assertEqual(len(subscription.invoices), 1)
+
+	def test_multicurrency_subscription(self):
+		subscription = frappe.new_doc('Subscription')
+		subscription.party_type = 'Customer'
+		subscription.party = '_Test Subscription Customer'
+		subscription.generate_invoice_at_period_start = 1
+		subscription.company = '_Test Company'
+		# select subscription start date as '2018-01-15'
+		subscription.start_date = '2018-01-01'
+		subscription.append('plans', {'plan': '_Test Plan Multicurrency', 'qty': 1})
+		subscription.save()
+
+		subscription.process()
+		self.assertEqual(len(subscription.invoices), 1)
+		self.assertEqual(subscription.status, 'Unpaid')
+
+		# Check the currency of the created invoice
+		currency = frappe.db.get_value('Sales Invoice', subscription.invoices[0].invoice, 'currency')
+		self.assertEqual(currency, 'USD')
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
index 878ae09..563df79 100644
--- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
@@ -75,7 +75,8 @@
    "fieldname": "cost",
    "fieldtype": "Currency",
    "in_list_view": 1,
-   "label": "Cost"
+   "label": "Cost",
+   "options": "currency"
   },
   {
    "depends_on": "eval:doc.price_determination==\"Based On Price List\"",
@@ -147,7 +148,7 @@
   }
  ],
  "links": [],
- "modified": "2021-08-13 10:53:44.205774",
+ "modified": "2021-12-10 15:24:15.794477",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Subscription Plan",
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 353f908..a990f23 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -545,7 +545,9 @@
 
 	def set_ageing(self, row):
 		if self.filters.ageing_based_on == "Due Date":
-			entry_date = row.due_date
+			# use posting date as a fallback for advances posted via journal and payment entry
+			# when ageing viewed by due date
+			entry_date = row.due_date or row.posting_date
 		elif self.filters.ageing_based_on == "Supplier Invoice Date":
 			entry_date = row.bill_date
 		else:
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/__init__.py b/erpnext/accounts/report/deferred_revenue_and_expense/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/__init__.py
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js
new file mode 100644
index 0000000..0056b9e
--- /dev/null
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js
@@ -0,0 +1,114 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+function get_filters() {
+	let filters = [
+		{
+			"fieldname":"company",
+			"label": __("Company"),
+			"fieldtype": "Link",
+			"options": "Company",
+			"default": frappe.defaults.get_user_default("Company"),
+			"reqd": 1
+		},
+		{
+			"fieldname":"filter_based_on",
+			"label": __("Filter Based On"),
+			"fieldtype": "Select",
+			"options": ["Fiscal Year", "Date Range"],
+			"default": ["Fiscal Year"],
+			"reqd": 1,
+			on_change: function() {
+				let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
+				frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
+				frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
+				frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
+				frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
+
+				frappe.query_report.refresh();
+			}
+		},
+		{
+			"fieldname":"period_start_date",
+			"label": __("Start Date"),
+			"fieldtype": "Date",
+			"hidden": 1,
+			"reqd": 1
+		},
+		{
+			"fieldname":"period_end_date",
+			"label": __("End Date"),
+			"fieldtype": "Date",
+			"hidden": 1,
+			"reqd": 1
+		},
+		{
+			"fieldname":"from_fiscal_year",
+			"label": __("Start Year"),
+			"fieldtype": "Link",
+			"options": "Fiscal Year",
+			"default": frappe.defaults.get_user_default("fiscal_year"),
+			"reqd": 1
+		},
+		{
+			"fieldname":"to_fiscal_year",
+			"label": __("End Year"),
+			"fieldtype": "Link",
+			"options": "Fiscal Year",
+			"default": frappe.defaults.get_user_default("fiscal_year"),
+			"reqd": 1
+		},
+		{
+			"fieldname": "periodicity",
+			"label": __("Periodicity"),
+			"fieldtype": "Select",
+			"options": [
+				{ "value": "Monthly", "label": __("Monthly") },
+				{ "value": "Quarterly", "label": __("Quarterly") },
+				{ "value": "Half-Yearly", "label": __("Half-Yearly") },
+				{ "value": "Yearly", "label": __("Yearly") }
+			],
+			"default": "Monthly",
+			"reqd": 1
+		},
+		{
+			"fieldname": "type",
+			"label": __("Invoice Type"),
+			"fieldtype": "Select",
+			"options": [
+				{ "value": "Revenue", "label": __("Revenue") },
+				{ "value": "Expense", "label": __("Expense") }
+			],
+			"default": "Revenue",
+			"reqd": 1
+		},
+		{
+			"fieldname" : "with_upcoming_postings",
+			"label": __("Show with upcoming revenue/expense"),
+			"fieldtype": "Check",
+			"default": 1
+		}
+	]
+
+	return filters;
+}
+
+frappe.query_reports["Deferred Revenue and Expense"] = {
+	"filters": get_filters(),
+	"formatter": function(value, row, column, data, default_formatter){
+		return default_formatter(value, row, column, data);
+	},
+	onload: function(report){
+		let fiscal_year = frappe.defaults.get_user_default("fiscal_year");
+
+		frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
+			var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
+			frappe.query_report.set_filter_value({
+				period_start_date: fy.year_start_date,
+				period_end_date: fy.year_end_date
+			});
+		});
+	}
+};
+
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.json b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.json
new file mode 100644
index 0000000..c7dfb3b
--- /dev/null
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-12-10 19:27:14.654220",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-12-10 19:27:14.654220",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Deferred Revenue and Expense",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "GL Entry",
+ "report_name": "Deferred Revenue and Expense",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Accounts User"
+  },
+  {
+   "role": "Accounts Manager"
+  },
+  {
+   "role": "Auditor"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
new file mode 100644
index 0000000..a4842c1
--- /dev/null
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
@@ -0,0 +1,440 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# License: MIT. See LICENSE
+
+import frappe
+from frappe import _, qb
+from frappe.query_builder import Column, functions
+from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, rounded
+
+from erpnext.accounts.report.financial_statements import get_period_list
+
+
+class Deferred_Item(object):
+	"""
+	Helper class for processing items with deferred revenue/expense
+	"""
+
+	def __init__(self, item, inv, gle_entries):
+		self.name = item
+		self.parent = inv.name
+		self.item_name = gle_entries[0].item_name
+		self.service_start_date = gle_entries[0].service_start_date
+		self.service_end_date = gle_entries[0].service_end_date
+		self.base_net_amount = gle_entries[0].base_net_amount
+		self.filters = inv.filters
+		self.period_list = inv.period_list
+
+		if gle_entries[0].deferred_revenue_account:
+			self.type = "Deferred Sale Item"
+			self.deferred_account = gle_entries[0].deferred_revenue_account
+		elif gle_entries[0].deferred_expense_account:
+			self.type = "Deferred Purchase Item"
+			self.deferred_account = gle_entries[0].deferred_expense_account
+
+		self.gle_entries = []
+		# holds period wise total for item
+		self.period_total = []
+		self.last_entry_date = self.service_start_date
+
+		if gle_entries:
+			self.gle_entries = gle_entries
+			for x in self.gle_entries:
+				if self.get_amount(x):
+					self.last_entry_date = x.gle_posting_date
+
+	def report_data(self):
+		"""
+		Generate report data for output
+		"""
+		ret_data = frappe._dict({"name": self.item_name})
+		for period in self.period_total:
+			ret_data[period.key] = period.total
+			ret_data.indent = 1
+		return ret_data
+
+	def get_amount(self, entry):
+		"""
+		For a given GL/Journal posting, get balance based on item type
+		"""
+		if self.type == "Deferred Sale Item":
+			return entry.debit - entry.credit
+		elif self.type == "Deferred Purchase Item":
+			return -(entry.credit - entry.debit)
+		return 0
+
+	def get_item_total(self):
+		"""
+		Helper method - calculate booked amount. Includes simulated postings as well
+		"""
+		total = 0
+		for gle_posting in self.gle_entries:
+			total += self.get_amount(gle_posting)
+
+		return total
+
+	def calculate_amount(self, start_date, end_date):
+		"""
+		start_date, end_date - datetime.datetime.date
+		return - estimated amount to post for given period
+		Calculated based on already booked amount and item service period
+		"""
+		total_months = (
+			(self.service_end_date.year - self.service_start_date.year) * 12
+			+ (self.service_end_date.month - self.service_start_date.month)
+			+ 1
+		)
+
+		prorate = date_diff(self.service_end_date, self.service_start_date) / date_diff(
+			get_last_day(self.service_end_date), get_first_day(self.service_start_date)
+		)
+
+		actual_months = rounded(total_months * prorate, 1)
+
+		already_booked_amount = self.get_item_total()
+		base_amount = self.base_net_amount / actual_months
+
+		if base_amount + already_booked_amount > self.base_net_amount:
+			base_amount = self.base_net_amount - already_booked_amount
+
+		if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date):
+			partial_month = flt(date_diff(end_date, start_date)) / flt(
+				date_diff(get_last_day(end_date), get_first_day(start_date))
+			)
+			base_amount *= rounded(partial_month, 1)
+
+		return base_amount
+
+	def make_dummy_gle(self, name, date, amount):
+		"""
+		return - frappe._dict() of a dummy gle entry
+		"""
+		entry = frappe._dict(
+			{"name": name, "gle_posting_date": date, "debit": 0, "credit": 0, "posted": "not"}
+		)
+		if self.type == "Deferred Sale Item":
+			entry.debit = amount
+		elif self.type == "Deferred Purchase Item":
+			entry.credit = amount
+		return entry
+
+	def simulate_future_posting(self):
+		"""
+		simulate future posting by creating dummy gl entries. starts from the last posting date.
+		"""
+		if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
+			self.estimate_for_period_list = get_period_list(
+				self.filters.from_fiscal_year,
+				self.filters.to_fiscal_year,
+				add_days(self.last_entry_date, 1),
+				self.period_list[-1].to_date,
+				"Date Range",
+				"Monthly",
+				company=self.filters.company,
+			)
+			for period in self.estimate_for_period_list:
+				amount = self.calculate_amount(period.from_date, period.to_date)
+				gle = self.make_dummy_gle(period.key, period.to_date, amount)
+				self.gle_entries.append(gle)
+
+	def calculate_item_revenue_expense_for_period(self):
+		"""
+		calculate item postings for each period and update period_total list
+		"""
+		for period in self.period_list:
+			period_sum = 0
+			actual = 0
+			for posting in self.gle_entries:
+				# if period.from_date <= posting.posting_date <= period.to_date:
+				if period.from_date <= posting.gle_posting_date <= period.to_date:
+					period_sum += self.get_amount(posting)
+					if posting.posted == "posted":
+						actual += self.get_amount(posting)
+
+			self.period_total.append(
+				frappe._dict({"key": period.key, "total": period_sum, "actual": actual})
+			)
+		return self.period_total
+
+
+class Deferred_Invoice(object):
+	def __init__(self, invoice, items, filters, period_list):
+		"""
+		Helper class for processing invoices with deferred revenue/expense items
+		invoice - string : invoice name
+		items - list : frappe._dict() with item details. Refer Deferred_Item for required fields
+		"""
+		self.name = invoice
+		self.posting_date = items[0].posting_date
+		self.filters = filters
+		self.period_list = period_list
+		# holds period wise total for invoice
+		self.period_total = []
+
+		if items[0].deferred_revenue_account:
+			self.type = "Sales"
+		elif items[0].deferred_expense_account:
+			self.type = "Purchase"
+
+		self.items = []
+		# for each uniq items
+		self.uniq_items = set([x.item for x in items])
+		for item in self.uniq_items:
+			self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
+
+	def calculate_invoice_revenue_expense_for_period(self):
+		"""
+		calculate deferred revenue/expense for all items in invoice
+		"""
+		# initialize period_total list for invoice
+		for period in self.period_list:
+			self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
+
+		for item in self.items:
+			item_total = item.calculate_item_revenue_expense_for_period()
+			# update invoice total
+			for idx, period in enumerate(self.period_list, 0):
+				self.period_total[idx].total += item_total[idx].total
+				self.period_total[idx].actual += item_total[idx].actual
+		return self.period_total
+
+	def estimate_future(self):
+		"""
+		create dummy GL entries for upcoming months for all items in invoice
+		"""
+		[item.simulate_future_posting() for item in self.items]
+
+	def report_data(self):
+		"""
+		generate report data for invoice, includes invoice total
+		"""
+		ret_data = []
+		inv_total = frappe._dict({"name": self.name})
+		for x in self.period_total:
+			inv_total[x.key] = x.total
+			inv_total.indent = 0
+		ret_data.append(inv_total)
+		list(map(lambda item: ret_data.append(item.report_data()), self.items))
+		return ret_data
+
+
+class Deferred_Revenue_and_Expense_Report(object):
+	def __init__(self, filters=None):
+		"""
+		Initialize deferred revenue/expense report with user provided filters or system defaults, if none is provided
+		"""
+
+		# If no filters are provided, get user defaults
+		if not filters:
+			fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
+			self.filters = frappe._dict(
+				{
+					"company": frappe.defaults.get_user_default("Company"),
+					"filter_based_on": "Fiscal Year",
+					"period_start_date": fiscal_year.year_start_date,
+					"period_end_date": fiscal_year.year_end_date,
+					"from_fiscal_year": fiscal_year.year,
+					"to_fiscal_year": fiscal_year.year,
+					"periodicity": "Monthly",
+					"type": "Revenue",
+					"with_upcoming_postings": True,
+				}
+			)
+		else:
+			self.filters = frappe._dict(filters)
+
+		self.period_list = None
+		self.deferred_invoices = []
+		# holds period wise total for report
+		self.period_total = []
+
+	def get_period_list(self):
+		"""
+		Figure out selected period based on filters
+		"""
+		self.period_list = get_period_list(
+			self.filters.from_fiscal_year,
+			self.filters.to_fiscal_year,
+			self.filters.period_start_date,
+			self.filters.period_end_date,
+			self.filters.filter_based_on,
+			self.filters.periodicity,
+			company=self.filters.company,
+		)
+
+	def get_invoices(self):
+		"""
+		Get all sales and purchase invoices which has deferred revenue/expense items
+		"""
+		gle = qb.DocType("GL Entry")
+		# column doesn't have an alias option
+		posted = Column("posted")
+
+		if self.filters.type == "Revenue":
+			inv = qb.DocType("Sales Invoice")
+			inv_item = qb.DocType("Sales Invoice Item")
+			deferred_flag_field = inv_item["enable_deferred_revenue"]
+			deferred_account_field = inv_item["deferred_revenue_account"]
+
+		elif self.filters.type == "Expense":
+			inv = qb.DocType("Purchase Invoice")
+			inv_item = qb.DocType("Purchase Invoice Item")
+			deferred_flag_field = inv_item["enable_deferred_expense"]
+			deferred_account_field = inv_item["deferred_expense_account"]
+
+		query = (
+			qb.from_(inv_item)
+			.join(inv)
+			.on(inv.name == inv_item.parent)
+			.join(gle)
+			.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
+			.select(
+				inv.name.as_("doc"),
+				inv.posting_date,
+				inv_item.name.as_("item"),
+				inv_item.item_name,
+				inv_item.service_start_date,
+				inv_item.service_end_date,
+				inv_item.base_net_amount,
+				deferred_account_field,
+				gle.posting_date.as_("gle_posting_date"),
+				functions.Sum(gle.debit).as_("debit"),
+				functions.Sum(gle.credit).as_("credit"),
+				posted,
+			)
+			.where(
+				(inv.docstatus == 1)
+				& (deferred_flag_field == 1)
+				& (
+					(
+						(self.period_list[0].from_date >= inv_item.service_start_date)
+						& (inv_item.service_end_date >= self.period_list[0].from_date)
+					)
+					| (
+						(inv_item.service_start_date >= self.period_list[0].from_date)
+						& (inv_item.service_start_date <= self.period_list[-1].to_date)
+					)
+				)
+			)
+			.groupby(inv.name, inv_item.name, gle.posting_date)
+			.orderby(gle.posting_date)
+		)
+		self.invoices = query.run(as_dict=True)
+
+		uniq_invoice = set([x.doc for x in self.invoices])
+		for inv in uniq_invoice:
+			self.deferred_invoices.append(
+				Deferred_Invoice(
+					inv, [x for x in self.invoices if x.doc == inv], self.filters, self.period_list
+				)
+			)
+
+	def estimate_future(self):
+		"""
+		For all Invoices estimate upcoming postings
+		"""
+		for x in self.deferred_invoices:
+			x.estimate_future()
+
+	def calculate_revenue_and_expense(self):
+		"""
+		calculate the deferred revenue/expense for all invoices
+		"""
+		# initialize period_total list for report
+		for period in self.period_list:
+			self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
+
+		for inv in self.deferred_invoices:
+			inv_total = inv.calculate_invoice_revenue_expense_for_period()
+			# calculate total for whole report
+			for idx, period in enumerate(self.period_list, 0):
+				self.period_total[idx].total += inv_total[idx].total
+				self.period_total[idx].actual += inv_total[idx].actual
+
+	def get_columns(self):
+		columns = []
+		columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
+		for period in self.period_list:
+			columns.append(
+				{
+					"label": _(period.label),
+					"fieldname": period.key,
+					"fieldtype": "Currency",
+					"read_only": 1,
+				})
+		return columns
+
+	def generate_report_data(self):
+		"""
+		Generate report data for all invoices. Adds total rows for revenue and expense
+		"""
+		ret = []
+
+		for inv in self.deferred_invoices:
+			ret += inv.report_data()
+
+		# empty row for padding
+		ret += [{}]
+
+		# add total row
+		if ret is not []:
+			if self.filters.type == "Revenue":
+				total_row = frappe._dict({"name": "Total Deferred Income"})
+			elif self.filters.type == "Expense":
+				total_row = frappe._dict({"name": "Total Deferred Expense"})
+
+			for idx, period in enumerate(self.period_list, 0):
+				total_row[period.key] = self.period_total[idx].total
+			ret.append(total_row)
+
+		return ret
+
+	def prepare_chart(self):
+		chart = {
+			"data": {
+				"labels": [period.label for period in self.period_list],
+				"datasets": [
+					{
+						"name": "Actual Posting",
+						"chartType": "bar",
+						"values": [x.actual for x in self.period_total],
+					}
+				],
+			},
+			"type": "axis-mixed",
+			"height": 500,
+			"axisOptions": {"xAxisMode": "Tick", "xIsSeries": True},
+			"barOptions": {"stacked": False, "spaceRatio": 0.5},
+		}
+
+		if self.filters.with_upcoming_postings:
+			chart["data"]["datasets"].append({
+				"name": "Expected",
+				"chartType": "line",
+				"values": [x.total for x in self.period_total]
+			})
+
+		return chart
+
+	def run(self, *args, **kwargs):
+		"""
+		Run report and generate data
+		"""
+		self.deferred_invoices.clear()
+		self.get_period_list()
+		self.get_invoices()
+
+		if self.filters.with_upcoming_postings:
+			self.estimate_future()
+		self.calculate_revenue_and_expense()
+
+
+def execute(filters=None):
+	report = Deferred_Revenue_and_Expense_Report(filters=filters)
+	report.run()
+
+	columns = report.get_columns()
+	data = report.generate_report_data()
+	message = []
+	chart = report.prepare_chart()
+
+	return columns, data, message, chart
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
new file mode 100644
index 0000000..1de6fb6
--- /dev/null
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
@@ -0,0 +1,253 @@
+import unittest
+
+import frappe
+from frappe import qb
+from frappe.utils import nowdate
+
+from erpnext.accounts.doctype.account.test_account import create_account
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import (
+	Deferred_Revenue_and_Expense_Report,
+)
+from erpnext.buying.doctype.supplier.test_supplier import create_supplier
+from erpnext.stock.doctype.item.test_item import create_item
+
+
+class TestDeferredRevenueAndExpense(unittest.TestCase):
+	@classmethod
+	def setUpClass(self):
+		clear_old_entries()
+		create_company()
+
+	def test_deferred_revenue(self):
+		# created deferred expense accounts, if not found
+		deferred_revenue_account = create_account(
+			account_name="Deferred Revenue",
+			parent_account="Current Liabilities - _CD",
+			company="_Test Company DR",
+		)
+
+		acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
+		acc_settings.book_deferred_entries_based_on = "Months"
+		acc_settings.save()
+
+		customer = frappe.new_doc("Customer")
+		customer.customer_name = "_Test Customer DR"
+		customer.type = "Individual"
+		customer.insert()
+
+		item = create_item(
+			"_Test Internet Subscription",
+			is_stock_item=0,
+			warehouse="All Warehouses - _CD",
+			company="_Test Company DR",
+		)
+		item.enable_deferred_revenue = 1
+		item.deferred_revenue_account = deferred_revenue_account
+		item.no_of_months = 3
+		item.save()
+
+		si = create_sales_invoice(
+			item=item.name,
+			company="_Test Company DR",
+			customer="_Test Customer DR",
+			debit_to="Debtors - _CD",
+			posting_date="2021-05-01",
+			parent_cost_center="Main - _CD",
+			cost_center="Main - _CD",
+			do_not_submit=True,
+			rate=300,
+			price_list_rate=300,
+		)
+		si.items[0].enable_deferred_revenue = 1
+		si.items[0].service_start_date = "2021-05-01"
+		si.items[0].service_end_date = "2021-08-01"
+		si.items[0].deferred_revenue_account = deferred_revenue_account
+		si.items[0].income_account = "Sales - _CD"
+		si.save()
+		si.submit()
+
+		pda = frappe.get_doc(
+			dict(
+				doctype="Process Deferred Accounting",
+				posting_date=nowdate(),
+				start_date="2021-05-01",
+				end_date="2021-08-01",
+				type="Income",
+				company="_Test Company DR",
+			)
+		)
+		pda.insert()
+		pda.submit()
+
+		# execute report
+		fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
+		self.filters = frappe._dict(
+			{
+				"company": frappe.defaults.get_user_default("Company"),
+				"filter_based_on": "Date Range",
+				"period_start_date": "2021-05-01",
+				"period_end_date": "2021-08-01",
+				"from_fiscal_year": fiscal_year.year,
+				"to_fiscal_year": fiscal_year.year,
+				"periodicity": "Monthly",
+				"type": "Revenue",
+				"with_upcoming_postings": False,
+			}
+		)
+
+		report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
+		report.run()
+		expected = [
+			{"key": "may_2021", "total": 100.0, "actual": 100.0},
+			{"key": "jun_2021", "total": 100.0, "actual": 100.0},
+			{"key": "jul_2021", "total": 100.0, "actual": 100.0},
+			{"key": "aug_2021", "total": 0, "actual": 0},
+		]
+		self.assertEqual(report.period_total, expected)
+
+	def test_deferred_expense(self):
+		# created deferred expense accounts, if not found
+		deferred_expense_account = create_account(
+			account_name="Deferred Expense",
+			parent_account="Current Assets - _CD",
+			company="_Test Company DR",
+		)
+
+		acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
+		acc_settings.book_deferred_entries_based_on = "Months"
+		acc_settings.save()
+
+		supplier = create_supplier(
+			supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company"
+		)
+		supplier.save()
+
+		item = create_item(
+			"_Test Office Desk",
+			is_stock_item=0,
+			warehouse="All Warehouses - _CD",
+			company="_Test Company DR",
+		)
+		item.enable_deferred_expense = 1
+		item.deferred_expense_account = deferred_expense_account
+		item.no_of_months_exp = 3
+		item.save()
+
+		pi = make_purchase_invoice(
+			item=item.name,
+			company="_Test Company DR",
+			supplier="_Test Furniture Supplier",
+			is_return=False,
+			update_stock=False,
+			posting_date=frappe.utils.datetime.date(2021, 5, 1),
+			parent_cost_center="Main - _CD",
+			cost_center="Main - _CD",
+			do_not_save=True,
+			rate=300,
+			price_list_rate=300,
+			warehouse="All Warehouses - _CD",
+			qty=1,
+		)
+		pi.set_posting_time = True
+		pi.items[0].enable_deferred_expense = 1
+		pi.items[0].service_start_date = "2021-05-01"
+		pi.items[0].service_end_date = "2021-08-01"
+		pi.items[0].deferred_expense_account = deferred_expense_account
+		pi.items[0].expense_account = "Office Maintenance Expenses - _CD"
+		pi.save()
+		pi.submit()
+
+		pda = frappe.get_doc(
+			dict(
+				doctype="Process Deferred Accounting",
+				posting_date=nowdate(),
+				start_date="2021-05-01",
+				end_date="2021-08-01",
+				type="Expense",
+				company="_Test Company DR",
+			)
+		)
+		pda.insert()
+		pda.submit()
+
+		# execute report
+		fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
+		self.filters = frappe._dict(
+			{
+				"company": frappe.defaults.get_user_default("Company"),
+				"filter_based_on": "Date Range",
+				"period_start_date": "2021-05-01",
+				"period_end_date": "2021-08-01",
+				"from_fiscal_year": fiscal_year.year,
+				"to_fiscal_year": fiscal_year.year,
+				"periodicity": "Monthly",
+				"type": "Expense",
+				"with_upcoming_postings": False,
+			}
+		)
+
+		report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
+		report.run()
+		expected = [
+			{"key": "may_2021", "total": -100.0, "actual": -100.0},
+			{"key": "jun_2021", "total": -100.0, "actual": -100.0},
+			{"key": "jul_2021", "total": -100.0, "actual": -100.0},
+			{"key": "aug_2021", "total": 0, "actual": 0},
+		]
+		self.assertEqual(report.period_total, expected)
+
+
+def create_company():
+	company = frappe.db.exists("Company", "_Test Company DR")
+	if not company:
+		company = frappe.new_doc("Company")
+		company.company_name = "_Test Company DR"
+		company.default_currency = "INR"
+		company.chart_of_accounts = "Standard"
+		company.insert()
+
+
+def clear_old_entries():
+	item = qb.DocType("Item")
+	account = qb.DocType("Account")
+	customer = qb.DocType("Customer")
+	supplier = qb.DocType("Supplier")
+	sinv = qb.DocType("Sales Invoice")
+	sinv_item = qb.DocType("Sales Invoice Item")
+	pinv = qb.DocType("Purchase Invoice")
+	pinv_item = qb.DocType("Purchase Invoice Item")
+
+	qb.from_(account).delete().where(
+		(account.account_name == "Deferred Revenue")
+		| (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR")
+	).run()
+	qb.from_(item).delete().where(
+		(item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent")
+	).run()
+	qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run()
+	qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run()
+
+	# delete existing invoices with deferred items
+	deferred_invoices = (
+		qb.from_(sinv)
+		.join(sinv_item)
+		.on(sinv.name == sinv_item.parent)
+		.select(sinv.name)
+		.where(sinv_item.enable_deferred_revenue == 1)
+		.run()
+	)
+	if deferred_invoices:
+		qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run()
+
+	deferred_invoices = (
+		qb.from_(pinv)
+		.join(pinv_item)
+		.on(pinv.name == pinv_item.parent)
+		.select(pinv.name)
+		.where(pinv_item.enable_deferred_expense == 1)
+		.run()
+	)
+	if deferred_invoices:
+		qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
index a3a45d1..caee1a1 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
@@ -36,12 +36,16 @@
 			posting_date = entry.posting_date
 			voucher_type = entry.voucher_type
 
+			if not tax_withholding_category:
+				tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category')
+				rate = tax_rate_map.get(tax_withholding_category)
+
 			if entry.account in tds_accounts:
 				tds_deducted += (entry.credit - entry.debit)
 
 			total_amount_credited += (entry.credit - entry.debit)
 
-		if rate and tds_deducted:
+		if tds_deducted:
 			row = {
 				'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
 				'supplier': supplier_map.get(supplier, {}).get('name')
@@ -67,7 +71,7 @@
 
 def get_supplier_pan_map():
 	supplier_map = frappe._dict()
-	suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name'])
+	suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category'])
 
 	for d in suppliers:
 		supplier_map[d.name] = d
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index c2b1bbc..153f5c5 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -110,7 +110,7 @@
 
 			if (frm.doc.status != 'Fully Depreciated') {
 				frm.add_custom_button(__("Adjust Asset Value"), function() {
-					frm.trigger("create_asset_adjustment");
+					frm.trigger("create_asset_value_adjustment");
 				}, __("Manage"));
 			}
 
@@ -322,14 +322,14 @@
 		});
 	},
 
-	create_asset_adjustment: function(frm) {
+	create_asset_value_adjustment: function(frm) {
 		frappe.call({
 			args: {
 				"asset": frm.doc.name,
 				"asset_category": frm.doc.asset_category,
 				"company": frm.doc.company
 			},
-			method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment",
+			method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment",
 			freeze: 1,
 			callback: function(r) {
 				var doclist = frappe.model.sync(r.message);
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 333906a..a18b03a 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -185,83 +185,84 @@
 		if not self.available_for_use_date:
 			return
 
-		for d in self.get('finance_books'):
-			self.validate_asset_finance_books(d)
+		start = self.clear_depreciation_schedule()
 
-			start = self.clear_depreciation_schedule()
+		for finance_book in self.get('finance_books'):
+			self.validate_asset_finance_books(finance_book)
 
 			# value_after_depreciation - current Asset value
-			if self.docstatus == 1 and d.value_after_depreciation:
-				value_after_depreciation = flt(d.value_after_depreciation)
+			if self.docstatus == 1 and finance_book.value_after_depreciation:
+				value_after_depreciation = flt(finance_book.value_after_depreciation)
 			else:
 				value_after_depreciation = (flt(self.gross_purchase_amount) -
 					flt(self.opening_accumulated_depreciation))
 
-			d.value_after_depreciation = value_after_depreciation
+			finance_book.value_after_depreciation = value_after_depreciation
 
-			number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
+			number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
 				cint(self.number_of_depreciations_booked)
 
-			has_pro_rata = self.check_is_pro_rata(d)
+			has_pro_rata = self.check_is_pro_rata(finance_book)
 
 			if has_pro_rata:
 				number_of_pending_depreciations += 1
 
 			skip_row = False
-			for n in range(start, number_of_pending_depreciations):
+
+			for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
 				# If depreciation is already completed (for double declining balance)
 				if skip_row: continue
 
-				depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d)
+				depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
 
 				if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
-					schedule_date = add_months(d.depreciation_start_date,
-						n * cint(d.frequency_of_depreciation))
+					schedule_date = add_months(finance_book.depreciation_start_date,
+						n * cint(finance_book.frequency_of_depreciation))
 
 					# schedule date will be a year later from start date
 					# so monthly schedule date is calculated by removing 11 months from it
-					monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1)
+					monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
 
 				# if asset is being sold
 				if date_of_sale:
-					from_date = self.get_from_date(d.finance_book)
-					depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
+					from_date = self.get_from_date(finance_book.finance_book)
+					depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
 						from_date, date_of_sale)
 
 					if depreciation_amount > 0:
 						self.append("schedules", {
 							"schedule_date": date_of_sale,
 							"depreciation_amount": depreciation_amount,
-							"depreciation_method": d.depreciation_method,
-							"finance_book": d.finance_book,
-							"finance_book_id": d.idx
+							"depreciation_method": finance_book.depreciation_method,
+							"finance_book": finance_book.finance_book,
+							"finance_book_id": finance_book.idx
 						})
 
 					break
 
 				# For first row
 				if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
-					depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
-						self.available_for_use_date, d.depreciation_start_date)
+					depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
+						self.available_for_use_date, finance_book.depreciation_start_date)
 
 					# For first depr schedule date will be the start date
 					# so monthly schedule date is calculated by removing month difference between use date and start date
-					monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1)
+					monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
 
 				# For last row
 				elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
 					if not self.flags.increase_in_asset_life:
 						# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
 						self.to_date = add_months(self.available_for_use_date,
-							(n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation))
+							(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
 
 					depreciation_amount_without_pro_rata = depreciation_amount
 
-					depreciation_amount, days, months = self.get_pro_rata_amt(d,
+					depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
 						depreciation_amount, schedule_date, self.to_date)
 
 					depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
-						depreciation_amount, d.finance_book)
+						depreciation_amount, finance_book.finance_book)
 
 					monthly_schedule_date = add_months(schedule_date, 1)
 					schedule_date = add_days(schedule_date, days)
@@ -272,10 +273,10 @@
 					self.precision("gross_purchase_amount"))
 
 				# Adjust depreciation amount in the last period based on the expected value after useful life
-				if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
-					and value_after_depreciation != d.expected_value_after_useful_life)
-					or value_after_depreciation < d.expected_value_after_useful_life):
-					depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
+				if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
+					and value_after_depreciation != finance_book.expected_value_after_useful_life)
+					or value_after_depreciation < finance_book.expected_value_after_useful_life):
+					depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
 					skip_row = True
 
 				if depreciation_amount > 0:
@@ -285,7 +286,7 @@
 						# In pro rata case, for first and last depreciation, month range would be different
 						month_range = months \
 							if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
-							else d.frequency_of_depreciation
+							else finance_book.frequency_of_depreciation
 
 						for r in range(month_range):
 							if (has_pro_rata and n == 0):
@@ -311,27 +312,52 @@
 							self.append("schedules", {
 								"schedule_date": date,
 								"depreciation_amount": amount,
-								"depreciation_method": d.depreciation_method,
-								"finance_book": d.finance_book,
-								"finance_book_id": d.idx
+								"depreciation_method": finance_book.depreciation_method,
+								"finance_book": finance_book.finance_book,
+								"finance_book_id": finance_book.idx
 							})
 					else:
 						self.append("schedules", {
 							"schedule_date": schedule_date,
 							"depreciation_amount": depreciation_amount,
-							"depreciation_method": d.depreciation_method,
-							"finance_book": d.finance_book,
-							"finance_book_id": d.idx
+							"depreciation_method": finance_book.depreciation_method,
+							"finance_book": finance_book.finance_book,
+							"finance_book_id": finance_book.idx
 						})
 
-	# used when depreciation schedule needs to be modified due to increase in asset life
+	# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
+	# JE: Journal Entry, FB: Finance Book
 	def clear_depreciation_schedule(self):
-		start = 0
-		for n in range(len(self.schedules)):
-			if not self.schedules[n].journal_entry:
-				del self.schedules[n:]
-				start = n
-				break
+		start = []
+		num_of_depreciations_completed = 0
+		depr_schedule = []
+
+		for schedule in self.get('schedules'):
+
+			# to update start when there are JEs linked with all the schedule rows corresponding to an FB
+			if len(start) == (int(schedule.finance_book_id) - 2):
+				start.append(num_of_depreciations_completed)
+				num_of_depreciations_completed = 0
+
+			# to ensure that start will only be updated once for each FB
+			if len(start) == (int(schedule.finance_book_id) - 1):
+				if schedule.journal_entry:
+					num_of_depreciations_completed += 1
+					depr_schedule.append(schedule)
+				else:
+					start.append(num_of_depreciations_completed)
+					num_of_depreciations_completed = 0
+
+		# to update start when all the schedule rows corresponding to the last FB are linked with JEs
+		if len(start) == (len(self.finance_books) - 1):
+			start.append(num_of_depreciations_completed)
+
+		# when the Depreciation Schedule is being created for the first time
+		if start == []:
+			start = [0] * len(self.finance_books)
+		else:
+			self.schedules = depr_schedule
+
 		return start
 
 	def get_from_date(self, finance_book):
@@ -469,7 +495,6 @@
 
 				asset_value_after_full_schedule = flt(
 					flt(self.gross_purchase_amount) -
-					flt(self.opening_accumulated_depreciation) -
 					flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
 
 				if (row.expected_value_after_useful_life and
@@ -731,14 +756,14 @@
 	return asset_repair
 
 @frappe.whitelist()
-def create_asset_adjustment(asset, asset_category, company):
-	asset_maintenance = frappe.get_doc("Asset Value Adjustment")
-	asset_maintenance.update({
+def create_asset_value_adjustment(asset, asset_category, company):
+	asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
+	asset_value_adjustment.update({
 		"asset": asset,
 		"company": company,
 		"asset_category": asset_category
 	})
-	return asset_maintenance
+	return asset_value_adjustment
 
 @frappe.whitelist()
 def transfer_asset(args):
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index ce2cb01..44c4ce5 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -955,6 +955,82 @@
 
 		self.assertEqual(len(asset.schedules), 1)
 
+	def test_clear_depreciation_schedule_for_multiple_finance_books(self):
+		asset = create_asset(
+			item_code = "Macbook Pro",
+			available_for_use_date = "2019-12-31",
+			do_not_save = 1
+		)
+
+		asset.calculate_depreciation = 1
+		asset.append("finance_books", {
+			"depreciation_method": "Straight Line",
+			"frequency_of_depreciation": 1,
+			"total_number_of_depreciations": 3,
+			"expected_value_after_useful_life": 10000,
+			"depreciation_start_date": "2020-01-31"
+		})
+		asset.append("finance_books", {
+			"depreciation_method": "Straight Line",
+			"frequency_of_depreciation": 1,
+			"total_number_of_depreciations": 6,
+			"expected_value_after_useful_life": 10000,
+			"depreciation_start_date": "2020-01-31"
+		})
+		asset.append("finance_books", {
+			"depreciation_method": "Straight Line",
+			"frequency_of_depreciation": 12,
+			"total_number_of_depreciations": 3,
+			"expected_value_after_useful_life": 10000,
+			"depreciation_start_date": "2020-12-31"
+		})
+		asset.submit()
+
+		post_depreciation_entries(date="2020-04-01")
+		asset.load_from_db()
+
+		asset.clear_depreciation_schedule()
+
+		self.assertEqual(len(asset.schedules), 6)
+
+		for schedule in asset.schedules:
+			if schedule.idx <= 3:
+				self.assertEqual(schedule.finance_book_id, "1")
+			else:
+				self.assertEqual(schedule.finance_book_id, "2")
+
+	def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
+		asset = create_asset(
+			item_code = "Macbook Pro",
+			available_for_use_date = "2019-12-31",
+			do_not_save = 1
+		)
+
+		asset.calculate_depreciation = 1
+		asset.append("finance_books", {
+			"depreciation_method": "Straight Line",
+			"frequency_of_depreciation": 12,
+			"total_number_of_depreciations": 3,
+			"expected_value_after_useful_life": 10000,
+			"depreciation_start_date": "2020-12-31"
+		})
+		asset.append("finance_books", {
+			"depreciation_method": "Straight Line",
+			"frequency_of_depreciation": 12,
+			"total_number_of_depreciations": 6,
+			"expected_value_after_useful_life": 10000,
+			"depreciation_start_date": "2020-12-31"
+		})
+		asset.save()
+
+		self.assertEqual(len(asset.schedules), 9)
+
+		for schedule in asset.schedules:
+			if schedule.idx <= 3:
+				self.assertEqual(schedule.finance_book_id, 1)
+			else:
+				self.assertEqual(schedule.finance_book_id, 2)
+
 	def test_depreciation_entry_cancellation(self):
 		asset = create_asset(
 			item_code = "Macbook Pro",
diff --git a/erpnext/assets/module_onboarding/assets/assets.json b/erpnext/assets/module_onboarding/assets/assets.json
index e6df88b..796245d 100644
--- a/erpnext/assets/module_onboarding/assets/assets.json
+++ b/erpnext/assets/module_onboarding/assets/assets.json
@@ -13,7 +13,7 @@
  "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/asset",
  "idx": 0,
  "is_complete": 0,
- "modified": "2021-08-24 17:50:41.573281",
+ "modified": "2021-12-02 11:24:37.963746",
  "modified_by": "Administrator",
  "module": "Assets",
  "name": "Assets",
diff --git a/erpnext/assets/onboarding_step/asset_category/asset_category.json b/erpnext/assets/onboarding_step/asset_category/asset_category.json
index 033e866..58f322e 100644
--- a/erpnext/assets/onboarding_step/asset_category/asset_category.json
+++ b/erpnext/assets/onboarding_step/asset_category/asset_category.json
@@ -9,7 +9,7 @@
  "is_complete": 0,
  "is_single": 0,
  "is_skipped": 0,
- "modified": "2021-08-24 12:49:37.665239",
+ "modified": "2021-11-23 10:02:03.242127",
  "modified_by": "Administrator",
  "name": "Asset Category",
  "owner": "Administrator",
diff --git a/erpnext/assets/onboarding_step/asset_item/asset_item.json b/erpnext/assets/onboarding_step/asset_item/asset_item.json
index 8a174c5..13e3e2e 100644
--- a/erpnext/assets/onboarding_step/asset_item/asset_item.json
+++ b/erpnext/assets/onboarding_step/asset_item/asset_item.json
@@ -1,21 +1,22 @@
 {
- "action": "Show Form Tour",
+ "action": "Create Entry",
  "action_label": "Let's create a new Asset item",
  "creation": "2021-08-13 14:27:07.277167",
  "description": "# Asset Item\n\nAsset items are created based on Asset Category. You can create one or multiple items against once Asset Category. The sales and purchase transaction for Asset is done via Asset Item. ",
  "docstatus": 0,
  "doctype": "Onboarding Step",
+ "form_tour": "Item",
  "idx": 0,
  "is_complete": 0,
  "is_single": 0,
  "is_skipped": 0,
- "modified": "2021-08-16 13:59:18.362233",
+ "modified": "2021-12-02 11:23:48.158504",
  "modified_by": "Administrator",
  "name": "Asset Item",
  "owner": "Administrator",
  "reference_document": "Item",
- "show_form_tour": 0,
- "show_full_form": 0,
+ "show_form_tour": 1,
+ "show_full_form": 1,
  "title": "Create an Asset Item",
  "validate_action": 1
 }
\ No newline at end of file
diff --git a/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json
index 54611ed..69fa337 100644
--- a/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json
+++ b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json
@@ -9,7 +9,7 @@
  "is_complete": 0,
  "is_single": 0,
  "is_skipped": 0,
- "modified": "2021-08-24 17:26:57.180637",
+ "modified": "2021-11-23 10:02:03.235498",
  "modified_by": "Administrator",
  "name": "Asset Purchase",
  "owner": "Administrator",
diff --git a/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json
index cebee7a..2fc6c46 100644
--- a/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json
+++ b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json
@@ -9,7 +9,7 @@
  "is_complete": 0,
  "is_single": 0,
  "is_skipped": 0,
- "modified": "2021-08-24 17:46:37.646174",
+ "modified": "2021-11-23 10:02:03.229566",
  "modified_by": "Administrator",
  "name": "Fixed Asset Accounts",
  "owner": "Administrator",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 5eab21b..1b5f35e 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -72,6 +72,7 @@
 		self.create_raw_materials_supplied("supplied_items")
 		self.set_received_qty_for_drop_ship_items()
 		validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_order_reference)
+		self.reset_default_field_value("set_warehouse", "items", "warehouse")
 
 	def validate_with_previous_doc(self):
 		super(PurchaseOrder, self).validate_with_previous_doc({
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index bde00cb..e7049fd 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -124,6 +124,14 @@
 		dialog.show()
 	},
 
+	schedule_date(frm) {
+		if(frm.doc.schedule_date){
+			frm.doc.items.forEach((item) => {
+				item.schedule_date = frm.doc.schedule_date;
+			})
+		}
+		refresh_field("items");
+	},
 	preview: (frm) => {
 		let dialog = new frappe.ui.Dialog({
 			title: __('Preview Email'),
@@ -184,7 +192,13 @@
 		dialog.show();
 	}
 })
-
+frappe.ui.form.on("Request for Quotation Item", {
+	items_add(frm, cdt, cdn) {
+		if (frm.doc.schedule_date) {
+			frappe.model.set_value(cdt, cdn, 'schedule_date', frm.doc.schedule_date);
+		}
+	}
+});
 frappe.ui.form.on("Request for Quotation Supplier",{
 	supplier: function(frm, cdt, cdn) {
 		var d = locals[cdt][cdn]
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
index 4ce4100..4993df9 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
@@ -12,6 +12,7 @@
   "vendor",
   "column_break1",
   "transaction_date",
+  "schedule_date",
   "status",
   "amended_from",
   "suppliers_section",
@@ -246,16 +247,22 @@
    "fieldname": "sec_break_email_2",
    "fieldtype": "Section Break",
    "hide_border": 1
+  },
+  {
+   "fieldname": "schedule_date",
+   "fieldtype": "Date",
+   "label": "Required Date"
   }
  ],
  "icon": "fa fa-shopping-cart",
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2020-11-05 22:04:29.017134",
+ "modified": "2021-11-24 17:47:49.909000",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Request for Quotation",
+ "naming_rule": "By \"Naming Series\" field",
  "owner": "Administrator",
  "permissions": [
   {
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
index 0a51a8e..023c95d 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
@@ -17,6 +17,7 @@
   "company",
   "transaction_date",
   "valid_till",
+  "quotation_number",
   "amended_from",
   "address_section",
   "supplier_address",
@@ -797,6 +798,11 @@
    "fieldtype": "Date",
    "in_list_view": 1,
    "label": "Valid Till"
+  },
+  {
+   "fieldname": "quotation_number",
+   "fieldtype": "Data",
+   "label": "Quotation Number"
   }
  ],
  "icon": "fa fa-shopping-cart",
@@ -804,10 +810,11 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-04-19 00:58:20.995491",
+ "modified": "2021-12-11 06:43:20.924080",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Supplier Quotation",
+ "naming_rule": "By \"Naming Series\" field",
  "owner": "Administrator",
  "permissions": [
   {
diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py
new file mode 100644
index 0000000..13aa697
--- /dev/null
+++ b/erpnext/controllers/tests/test_transaction_base.py
@@ -0,0 +1,22 @@
+import unittest
+
+import frappe
+
+
+class TestUtils(unittest.TestCase):
+    def test_reset_default_field_value(self):
+        doc = frappe.get_doc({
+            "doctype": "Purchase Receipt",
+            "set_warehouse": "Warehouse 1",
+        })
+
+        # Same values
+        doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
+        doc.reset_default_field_value("set_warehouse", "items", "warehouse")
+        self.assertEqual(doc.set_warehouse, "Warehouse 1")
+
+        # Mixed values
+        doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
+        doc.reset_default_field_value("set_warehouse", "items", "warehouse")
+        self.assertEqual(doc.set_warehouse, None)
+
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json
index 95b19fa..8f0fa31 100644
--- a/erpnext/crm/doctype/crm_settings/crm_settings.json
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.json
@@ -91,8 +91,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "migration_hash": "3ae78b12dd1c64d551736c6e82092f90",
- "modified": "2021-11-03 09:00:36.883496",
+ "modified": "2021-11-03 10:00:36.883496",
  "modified_by": "Administrator",
  "module": "CRM",
  "name": "CRM Settings",
diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json
index feb6044..dc32d9a 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.json
+++ b/erpnext/crm/doctype/opportunity/opportunity.json
@@ -510,8 +510,7 @@
  "icon": "fa fa-info-sign",
  "idx": 195,
  "links": [],
- "migration_hash": "d87c646ea2579b6900197fd41e6c5c5a",
- "modified": "2021-10-21 11:04:30.151379",
+ "modified": "2021-10-21 12:04:30.151379",
  "modified_by": "Administrator",
  "module": "CRM",
  "name": "Opportunity",
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
index ae1f36e..6afd3f7 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
@@ -115,8 +115,7 @@
  ],
  "issingle": 1,
  "links": [],
- "migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f",
- "modified": "2021-11-30 11:17:24.647979",
+ "modified": "2021-11-30 12:17:24.647979",
  "modified_by": "Administrator",
  "module": "ERPNext Integrations",
  "name": "TaxJar Settings",
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 05c46c5..9ceb626 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -234,7 +234,7 @@
 	},
 	"Communication": {
 		"on_update": [
-			"erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time",
+			"erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update",
 			"erpnext.support.doctype.issue.issue.set_first_response_time"
 		]
 	},
@@ -265,6 +265,9 @@
 			"erpnext.regional.india.utils.update_taxable_values"
 		]
 	},
+	"POS Invoice": {
+		"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]
+	},
 	"Purchase Invoice": {
 		"validate": [
 			"erpnext.regional.india.utils.validate_reverse_charge_transaction",
@@ -340,8 +343,7 @@
 		"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
 		"erpnext.projects.doctype.project.project.hourly_reminder",
 		"erpnext.projects.doctype.project.project.collect_project_status",
-		"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
-		"erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance"
+		"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts"
 	],
 	"hourly_long": [
 		"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py
index a4c0af0..a1247d9 100644
--- a/erpnext/hr/doctype/employee/employee_dashboard.py
+++ b/erpnext/hr/doctype/employee/employee_dashboard.py
@@ -21,7 +21,11 @@
 			},
 			{
 				'label': _('Lifecycle'),
-				'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
+				'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance']
+			},
+			{
+				'label': _('Exit'),
+				'items': ['Employee Separation', 'Exit Interview', 'Full and Final Statement']
 			},
 			{
 				'label': _('Shift'),
diff --git a/erpnext/hr/doctype/exit_interview/__init__.py b/erpnext/hr/doctype/exit_interview/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/__init__.py
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js
new file mode 100644
index 0000000..502af42
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/exit_interview.js
@@ -0,0 +1,38 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Exit Interview', {
+	refresh: function(frm) {
+		if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent && frappe.boot.user.can_write.includes('Exit Interview')) {
+			frm.add_custom_button(__('Send Exit Questionnaire'), function () {
+				frm.trigger('send_exit_questionnaire');
+			});
+		}
+	},
+
+	employee: function(frm) {
+		frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date', (message) => {
+			if (!message.relieving_date) {
+				frappe.throw({
+					message: __('Please set the relieving date for employee {0}',
+						['<a href="/app/employee/' + frm.doc.employee +'">' + frm.doc.employee + '</a>']),
+					title: __('Relieving Date Missing')
+				});
+			}
+		});
+	},
+
+	send_exit_questionnaire: function(frm) {
+		frappe.call({
+			method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
+			args: {
+				'interviews': [frm.doc]
+			},
+			callback: function(r) {
+				if (!r.exc) {
+					frm.refresh_field('questionnaire_email_sent');
+				}
+			}
+		});
+	}
+});
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json
new file mode 100644
index 0000000..989a1b8
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/exit_interview.json
@@ -0,0 +1,246 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "naming_series:",
+ "creation": "2021-12-05 13:56:36.241690",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "email_append_to": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "naming_series",
+  "employee",
+  "employee_name",
+  "email",
+  "column_break_5",
+  "company",
+  "status",
+  "date",
+  "employee_details_section",
+  "department",
+  "designation",
+  "reports_to",
+  "column_break_9",
+  "date_of_joining",
+  "relieving_date",
+  "exit_questionnaire_section",
+  "ref_doctype",
+  "questionnaire_email_sent",
+  "column_break_10",
+  "reference_document_name",
+  "interview_summary_section",
+  "interviewers",
+  "interview_summary",
+  "employee_status_section",
+  "employee_status",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "employee",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Employee",
+   "options": "Employee",
+   "reqd": 1
+  },
+  {
+   "fetch_from": "employee.employee_name",
+   "fieldname": "employee_name",
+   "fieldtype": "Data",
+   "label": "Employee Name",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "employee.department",
+   "fieldname": "department",
+   "fieldtype": "Link",
+   "label": "Department",
+   "options": "Department",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "employee.relieving_date",
+   "fieldname": "relieving_date",
+   "fieldtype": "Date",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Relieving Date",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_5",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "in_standard_filter": 1,
+   "label": "Company",
+   "options": "Company",
+   "reqd": 1
+  },
+  {
+   "fieldname": "date",
+   "fieldtype": "Date",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Date",
+   "mandatory_depends_on": "eval:doc.status==='Scheduled';"
+  },
+  {
+   "fieldname": "exit_questionnaire_section",
+   "fieldtype": "Section Break",
+   "label": "Exit Questionnaire"
+  },
+  {
+   "fieldname": "ref_doctype",
+   "fieldtype": "Link",
+   "label": "Reference Document Type",
+   "options": "DocType"
+  },
+  {
+   "fieldname": "reference_document_name",
+   "fieldtype": "Dynamic Link",
+   "in_list_view": 1,
+   "label": "Reference Document Name",
+   "options": "ref_doctype"
+  },
+  {
+   "fieldname": "interview_summary_section",
+   "fieldtype": "Section Break",
+   "label": "Interview Details"
+  },
+  {
+   "fieldname": "column_break_10",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "interviewers",
+   "fieldtype": "Table MultiSelect",
+   "label": "Interviewers",
+   "mandatory_depends_on": "eval:doc.status==='Scheduled';",
+   "options": "Interviewer"
+  },
+  {
+   "fetch_from": "employee.date_of_joining",
+   "fieldname": "date_of_joining",
+   "fieldtype": "Date",
+   "label": "Date of Joining",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "employee.reports_to",
+   "fieldname": "reports_to",
+   "fieldtype": "Link",
+   "in_standard_filter": 1,
+   "label": "Reports To",
+   "options": "Employee",
+   "read_only": 1
+  },
+  {
+   "fieldname": "employee_details_section",
+   "fieldtype": "Section Break",
+   "label": "Employee Details"
+  },
+  {
+   "fetch_from": "employee.designation",
+   "fieldname": "designation",
+   "fieldtype": "Link",
+   "label": "Designation",
+   "options": "Designation",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_9",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "naming_series",
+   "fieldtype": "Select",
+   "label": "Naming Series",
+   "options": "HR-EXIT-INT-"
+  },
+  {
+   "default": "0",
+   "fieldname": "questionnaire_email_sent",
+   "fieldtype": "Check",
+   "in_standard_filter": 1,
+   "label": "Questionnaire Email Sent",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "email",
+   "fieldtype": "Data",
+   "label": "Email ID",
+   "options": "Email",
+   "read_only": 1
+  },
+  {
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Status",
+   "options": "Pending\nScheduled\nCompleted\nCancelled",
+   "reqd": 1
+  },
+  {
+   "fieldname": "employee_status_section",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "employee_status",
+   "fieldtype": "Select",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Final Decision",
+   "mandatory_depends_on": "eval:doc.status==='Completed';",
+   "options": "\nEmployee Retained\nExit Confirmed"
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Exit Interview",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "interview_summary",
+   "fieldtype": "Text Editor",
+   "label": "Interview Summary"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2021-12-07 23:39:22.645401",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Exit Interview",
+ "naming_rule": "By \"Naming Series\" field",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sender_field": "email",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "employee_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py
new file mode 100644
index 0000000..30e19f1
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/exit_interview.py
@@ -0,0 +1,131 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import get_link_to_form
+
+from erpnext.hr.doctype.employee.employee import get_employee_email
+
+
+class ExitInterview(Document):
+	def validate(self):
+		self.validate_relieving_date()
+		self.validate_duplicate_interview()
+		self.set_employee_email()
+
+	def validate_relieving_date(self):
+		if not frappe.db.get_value('Employee', self.employee, 'relieving_date'):
+			frappe.throw(_('Please set the relieving date for employee {0}').format(
+				get_link_to_form('Employee', self.employee)),
+				title=_('Relieving Date Missing'))
+
+	def validate_duplicate_interview(self):
+		doc = frappe.db.exists('Exit Interview', {
+			'employee': self.employee,
+			'name': ('!=', self.name),
+			'docstatus': ('!=', 2)
+		})
+		if doc:
+			frappe.throw(_('Exit Interview {0} already exists for Employee: {1}').format(
+				get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)),
+				frappe.DuplicateEntryError)
+
+	def set_employee_email(self):
+		employee = frappe.get_doc('Employee', self.employee)
+		self.email = get_employee_email(employee)
+
+	def on_submit(self):
+		if self.status != 'Completed':
+			frappe.throw(_('Only Completed documents can be submitted'))
+
+		self.update_interview_date_in_employee()
+
+	def on_cancel(self):
+		self.update_interview_date_in_employee()
+		self.db_set('status', 'Cancelled')
+
+	def update_interview_date_in_employee(self):
+		if self.docstatus == 1:
+			frappe.db.set_value('Employee', self.employee, 'held_on', self.date)
+		elif self.docstatus == 2:
+			frappe.db.set_value('Employee', self.employee, 'held_on', None)
+
+
+@frappe.whitelist()
+def send_exit_questionnaire(interviews):
+	interviews = get_interviews(interviews)
+	validate_questionnaire_settings()
+
+	email_success = []
+	email_failure = []
+
+	for exit_interview in interviews:
+		interview = frappe.get_doc('Exit Interview', exit_interview.get('name'))
+		if interview.get('questionnaire_email_sent'):
+			continue
+
+		employee = frappe.get_doc('Employee', interview.employee)
+		email = get_employee_email(employee)
+
+		context = interview.as_dict()
+		context.update(employee.as_dict())
+		template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template')
+		template = frappe.get_doc('Email Template', template_name)
+
+		if email:
+			frappe.sendmail(
+				recipients=email,
+				subject=template.subject,
+				message=frappe.render_template(template.response, context),
+				reference_doctype=interview.doctype,
+				reference_name=interview.name
+			)
+			interview.db_set('questionnaire_email_sent', True)
+			interview.notify_update()
+			email_success.append(email)
+		else:
+			email_failure.append(get_link_to_form('Employee', employee.name))
+
+	show_email_summary(email_success, email_failure)
+
+
+def get_interviews(interviews):
+	import json
+
+	if isinstance(interviews, str):
+		interviews = json.loads(interviews)
+
+	if not len(interviews):
+		frappe.throw(_('Atleast one interview has to be selected.'))
+
+	return interviews
+
+
+def validate_questionnaire_settings():
+	settings = frappe.db.get_value('HR Settings', 'HR Settings',
+		['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], as_dict=True)
+
+	if not settings.exit_questionnaire_web_form or not settings.exit_questionnaire_notification_template:
+		frappe.throw(
+			_('Please set {0} and {1} in {2}.').format(
+				frappe.bold('Exit Questionnaire Web Form'),
+				frappe.bold('Notification Template'),
+				get_link_to_form('HR Settings', 'HR Settings')),
+			title=_('Settings Missing')
+		)
+
+
+def show_email_summary(email_success, email_failure):
+	message = ''
+	if email_success:
+		message += _('{0}: {1}').format(
+			frappe.bold('Sent Successfully'), ', '.join(email_success))
+	if message and email_failure:
+		message += '<br><br>'
+	if email_failure:
+		message += _('{0} due to missing email information for employee(s): {1}').format(
+			frappe.bold('Sending Failed'), ', '.join(email_failure))
+
+	frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)
\ No newline at end of file
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview_list.js b/erpnext/hr/doctype/exit_interview/exit_interview_list.js
new file mode 100644
index 0000000..93d7b21
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/exit_interview_list.js
@@ -0,0 +1,27 @@
+frappe.listview_settings['Exit Interview'] = {
+	has_indicator_for_draft: 1,
+	get_indicator: function(doc) {
+		let status_color = {
+			'Pending': 'orange',
+			'Scheduled': 'yellow',
+			'Completed': 'green',
+			'Cancelled': 'red',
+		};
+		return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
+	},
+
+	onload: function(listview) {
+		if (frappe.boot.user.can_write.includes('Exit Interview')) {
+			listview.page.add_action_item(__('Send Exit Questionnaires'), function() {
+				const interviews = listview.get_checked_items();
+				frappe.call({
+					method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
+					freeze: true,
+					args: {
+						'interviews': interviews
+					}
+				});
+			});
+		}
+	}
+};
diff --git a/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html b/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html
new file mode 100644
index 0000000..0317b1a
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html
@@ -0,0 +1,16 @@
+<h2>Exit Questionnaire</h2>
+<br>
+
+<p>
+	Dear {{ employee_name }},
+	<br><br>
+
+	Thank you for the contribution you have made during your time at {{ company }}. We value your opinion and welcome the feedback on your experience working with us.
+	Request you to take out a few minutes to fill up this Exit Questionnaire.
+
+	{% set web_form = frappe.db.get_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form') %}
+	{% set web_form_link = frappe.utils.get_url(uri=frappe.db.get_value('Web Form', web_form, 'route')) %}
+
+	<br><br>
+	<a class="btn btn-primary" href="{{ web_form_link }}" target="_blank">{{ _('Submit Now') }}</a>
+</p>
diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py
new file mode 100644
index 0000000..8e076ed
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py
@@ -0,0 +1,118 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import os
+import unittest
+
+import frappe
+from frappe import _
+from frappe.core.doctype.user_permission.test_user_permission import create_user
+from frappe.tests.test_webform import create_custom_doctype, create_webform
+from frappe.utils import getdate
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.exit_interview.exit_interview import send_exit_questionnaire
+
+
+class TestExitInterview(unittest.TestCase):
+	def setUp(self):
+		frappe.db.sql('delete from `tabExit Interview`')
+
+	def test_duplicate_interview(self):
+		employee = make_employee('employeeexitint1@example.com')
+		frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
+		interview = create_exit_interview(employee)
+
+		doc = frappe.copy_doc(interview)
+		self.assertRaises(frappe.DuplicateEntryError, doc.save)
+
+	def test_relieving_date_validation(self):
+		employee = make_employee('employeeexitint2@example.com')
+		# unset relieving date
+		frappe.db.set_value('Employee', employee, 'relieving_date', None)
+
+		interview = create_exit_interview(employee, save=False)
+		self.assertRaises(frappe.ValidationError, interview.save)
+
+		# set relieving date
+		frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
+		interview = create_exit_interview(employee)
+		self.assertTrue(interview.name)
+
+	def test_interview_date_updated_in_employee_master(self):
+		employee = make_employee('employeeexit3@example.com')
+		frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
+
+		interview = create_exit_interview(employee)
+		interview.status = 'Completed'
+		interview.employee_status = 'Exit Confirmed'
+
+		# exit interview date updated on submit
+		interview.submit()
+		self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), interview.date)
+
+		# exit interview reset on cancel
+		interview.reload()
+		interview.cancel()
+		self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), None)
+
+	def test_send_exit_questionnaire(self):
+		create_custom_doctype()
+		create_webform()
+		template = create_notification_template()
+
+		webform = frappe.db.get_all('Web Form', limit=1)
+		frappe.db.set_value('HR Settings', 'HR Settings', {
+			'exit_questionnaire_web_form': webform[0].name,
+			'exit_questionnaire_notification_template': template
+		})
+
+		employee = make_employee('employeeexit3@example.com')
+		frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
+
+		interview = create_exit_interview(employee)
+		send_exit_questionnaire([interview])
+
+		email_queue = frappe.db.get_all('Email Queue', ['name', 'message'], limit=1)
+		self.assertTrue('Subject: Exit Questionnaire Notification' in email_queue[0].message)
+
+	def tearDown(self):
+		frappe.db.rollback()
+
+
+def create_exit_interview(employee, save=True):
+	interviewer = create_user('test_exit_interviewer@example.com')
+
+	doc = frappe.get_doc({
+		'doctype': 'Exit Interview',
+		'employee': employee,
+		'company': '_Test Company',
+		'status': 'Pending',
+		'date': getdate(),
+		'interviewers': [{
+			'interviewer': interviewer.name
+		}],
+		'interview_summary': 'Test'
+	})
+
+	if save:
+		return doc.insert()
+	return doc
+
+
+def create_notification_template():
+	template = frappe.db.exists('Email Template', _('Exit Questionnaire Notification'))
+	if not template:
+		base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+		response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html'))
+
+		template = frappe.get_doc({
+			'doctype': 'Email Template',
+			'name': _('Exit Questionnaire Notification'),
+			'response': response,
+			'subject': _('Exit Questionnaire Notification'),
+			'owner': frappe.session.user,
+		}).insert(ignore_permissions=True)
+		template = template.name
+
+	return template
\ No newline at end of file
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 5148435..f9a3e05 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -36,7 +36,11 @@
   "remind_before",
   "column_break_4",
   "send_interview_feedback_reminder",
-  "feedback_reminder_notification_template"
+  "feedback_reminder_notification_template",
+  "employee_exit_section",
+  "exit_questionnaire_web_form",
+  "column_break_34",
+  "exit_questionnaire_notification_template"
  ],
  "fields": [
   {
@@ -226,13 +230,34 @@
    "fieldname": "check_vacancies",
    "fieldtype": "Check",
    "label": "Check Vacancies On Job Offer Creation"
+  },
+  {
+   "fieldname": "employee_exit_section",
+   "fieldtype": "Section Break",
+   "label": "Employee Exit Settings"
+  },
+  {
+   "fieldname": "exit_questionnaire_web_form",
+   "fieldtype": "Link",
+   "label": "Exit Questionnaire Web Form",
+   "options": "Web Form"
+  },
+  {
+   "fieldname": "exit_questionnaire_notification_template",
+   "fieldtype": "Link",
+   "label": "Exit Questionnaire Notification Template",
+   "options": "Email Template"
+  },
+  {
+   "fieldname": "column_break_34",
+   "fieldtype": "Column Break"
   }
  ],
  "icon": "fa fa-cog",
  "idx": 1,
  "issingle": 1,
  "links": [],
- "modified": "2021-10-01 23:46:11.098236",
+ "modified": "2021-12-05 14:48:10.884253",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "HR Settings",
diff --git a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
index 4185f28..d2ec5b9 100644
--- a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
+++ b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
@@ -59,7 +59,7 @@
 		}, 'average_rating')
 
 		# 1. average should be reflected in Interview Detail.
-		self.assertEqual(avg_on_interview_detail, round(feedback_1.average_rating))
+		self.assertEqual(avg_on_interview_detail, feedback_1.average_rating)
 
 		'''For Second Interviewer Feedback'''
 		interviewer = interview.interview_details[1].interviewer
diff --git a/erpnext/hr/notification/exit_interview_scheduled/__init__.py b/erpnext/hr/notification/exit_interview_scheduled/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/notification/exit_interview_scheduled/__init__.py
diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json
new file mode 100644
index 0000000..8323ef0
--- /dev/null
+++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json
@@ -0,0 +1,29 @@
+{
+ "attach_print": 0,
+ "channel": "Email",
+ "condition": "doc.date and doc.email and doc.docstatus != 2 and doc.status == 'Scheduled'",
+ "creation": "2021-12-05 22:11:47.263933",
+ "date_changed": "date",
+ "days_in_advance": 1,
+ "docstatus": 0,
+ "doctype": "Notification",
+ "document_type": "Exit Interview",
+ "enabled": 1,
+ "event": "Days Before",
+ "idx": 0,
+ "is_standard": 1,
+ "message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n\t<tr height=\"10\"></tr>\n\t<tr>\n\t\t<td width=\"15\"></td>\n\t\t<td>\n\t\t\t<div class=\"text-medium text-muted\">\n\t\t\t\t<span>{{_(\"Exit Interview Scheduled:\")}} {{ doc.name }}</span>\n\t\t\t</div>\n\t\t</td>\n\t\t<td width=\"15\"></td>\n\t</tr>\n\t<tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n\t<tr height=\"10\"></tr>\n\t<tr>\n\t\t<td width=\"15\"></td>\n\t\t<td>\n\t\t\t<div>\n\t\t\t\t<ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n\t\t\t\t\t<li>{{_(\"Employee\")}}: <b>{{ doc.employee }} - {{ doc.employee_name }}</b></li>\n\t\t\t\t\t<li>{{_(\"Date\")}}: <b>{{ doc.date }}</b></li>\n\t\t\t\t\t<li> {{_(\"Interviewers\")}}: </li>\n\t\t\t\t\t{% for entry in doc.interviewers %}\n\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t<li>{{ entry.user }}</li>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t{% endfor %}\n\t\t\t\t\t<li>{{ _(\"Interview Document\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n\t\t\t\t</ul>\n\t\t\t</div>\n\t\t</td>\n\t\t<td width=\"15\"></td>\n\t</tr>\n\t<tr height=\"10\"></tr>\n</table>\n",
+ "modified": "2021-12-05 22:26:57.096159",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Exit Interview Scheduled",
+ "owner": "Administrator",
+ "recipients": [
+  {
+   "receiver_by_document_field": "email"
+  }
+ ],
+ "send_system_notification": 0,
+ "send_to_all_assignees": 1,
+ "subject": "Exit Interview Scheduled: {{ doc.name }}"
+}
\ No newline at end of file
diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md
new file mode 100644
index 0000000..6d6db40
--- /dev/null
+++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md
@@ -0,0 +1,37 @@
+<table class="panel-header" border="0" cellpadding="0" cellspacing="0" width="100%">
+	<tr height="10"></tr>
+	<tr>
+		<td width="15"></td>
+		<td>
+			<div class="text-medium text-muted">
+				<h2>{{_("Exit Interview Scheduled:")}} {{ doc.name }}</h2>
+			</div>
+		</td>
+		<td width="15"></td>
+	</tr>
+	<tr height="10"></tr>
+</table>
+
+<table class="panel-body" border="0" cellpadding="0" cellspacing="0" width="100%">
+	<tr height="10"></tr>
+	<tr>
+		<td width="15"></td>
+		<td>
+			<div>
+				<ul class="list-unstyled" style="line-height: 1.7">
+					<li><b>{{_("Employee")}}: </b>{{ doc.employee }} - {{ doc.employee_name }}</li>
+					<li><b>{{_("Date")}}: </b>{{ frappe.utils.formatdate(doc.date) }}</li>
+					<li><b>{{_("Interviewers")}}:</b> </li>
+					{% for entry in doc.interviewers %}
+						<ul>
+							<li>{{ entry.user }}</li>
+						</ul>
+					{% endfor %}
+					<li><b>{{ _("Interview Document") }}:</b> {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
+				</ul>
+			</div>
+		</td>
+		<td width="15"></td>
+	</tr>
+	<tr height="10"></tr>
+</table>
diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py
new file mode 100644
index 0000000..5f697c9
--- /dev/null
+++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py
@@ -0,0 +1,6 @@
+# import frappe
+
+
+def get_context(context):
+	# do your magic here
+	pass
diff --git a/erpnext/hr/report/employee_exits/__init__.py b/erpnext/hr/report/employee_exits/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/report/employee_exits/__init__.py
diff --git a/erpnext/hr/report/employee_exits/employee_exits.js b/erpnext/hr/report/employee_exits/employee_exits.js
new file mode 100644
index 0000000..ac677d8
--- /dev/null
+++ b/erpnext/hr/report/employee_exits/employee_exits.js
@@ -0,0 +1,77 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Employee Exits"] = {
+	filters: [
+		{
+			"fieldname": "from_date",
+			"label": __("From Date"),
+			"fieldtype": "Date",
+			"default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12)
+		},
+		{
+			"fieldname": "to_date",
+			"label": __("To Date"),
+			"fieldtype": "Date",
+			"default": frappe.datetime.nowdate()
+		},
+		{
+			"fieldname": "company",
+			"label": __("Company"),
+			"fieldtype": "Link",
+			"options": "Company"
+		},
+		{
+			"fieldname": "department",
+			"label": __("Department"),
+			"fieldtype": "Link",
+			"options": "Department"
+		},
+		{
+			"fieldname": "designation",
+			"label": __("Designation"),
+			"fieldtype": "Link",
+			"options": "Designation"
+		},
+		{
+			"fieldname": "employee",
+			"label": __("Employee"),
+			"fieldtype": "Link",
+			"options": "Employee"
+		},
+		{
+			"fieldname": "reports_to",
+			"label": __("Reports To"),
+			"fieldtype": "Link",
+			"options": "Employee"
+		},
+		{
+			"fieldname": "interview_status",
+			"label": __("Interview Status"),
+			"fieldtype": "Select",
+			"options": ["", "Pending", "Scheduled", "Completed"]
+		},
+		{
+			"fieldname": "final_decision",
+			"label": __("Final Decision"),
+			"fieldtype": "Select",
+			"options": ["", "Employee Retained", "Exit Confirmed"]
+		},
+		{
+			"fieldname": "exit_interview_pending",
+			"label": __("Exit Interview Pending"),
+			"fieldtype": "Check"
+		},
+		{
+			"fieldname": "questionnaire_pending",
+			"label": __("Exit Questionnaire Pending"),
+			"fieldtype": "Check"
+		},
+		{
+			"fieldname": "fnf_pending",
+			"label": __("FnF Pending"),
+			"fieldtype": "Check"
+		}
+	]
+};
diff --git a/erpnext/hr/report/employee_exits/employee_exits.json b/erpnext/hr/report/employee_exits/employee_exits.json
new file mode 100644
index 0000000..4fe9a85
--- /dev/null
+++ b/erpnext/hr/report/employee_exits/employee_exits.json
@@ -0,0 +1,33 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-12-05 19:47:18.332319",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "Test",
+ "modified": "2021-12-05 19:47:18.332319",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Employee Exits",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Exit Interview",
+ "report_name": "Employee Exits",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "System Manager"
+  },
+  {
+   "role": "HR Manager"
+  },
+  {
+   "role": "HR User"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py
new file mode 100644
index 0000000..bde5a89
--- /dev/null
+++ b/erpnext/hr/report/employee_exits/employee_exits.py
@@ -0,0 +1,230 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# License: MIT. See LICENSE
+
+import frappe
+from frappe import _
+from frappe.query_builder import Order
+from frappe.utils import getdate
+
+
+def execute(filters=None):
+	columns = get_columns()
+	data = get_data(filters)
+	chart = get_chart_data(data)
+	report_summary = get_report_summary(data)
+
+	return columns, data, None, chart, report_summary
+
+def get_columns():
+	return [
+		{
+			'label': _('Employee'),
+			'fieldname': 'employee',
+			'fieldtype': 'Link',
+			'options': 'Employee',
+			'width': 150
+		},
+		{
+			'label': _('Employee Name'),
+			'fieldname': 'employee_name',
+			'fieldtype': 'Data',
+			'width': 150
+		},
+		{
+			'label': _('Date of Joining'),
+			'fieldname': 'date_of_joining',
+			'fieldtype': 'Date',
+			'width': 120
+		},
+		{
+			'label': _('Relieving Date'),
+			'fieldname': 'relieving_date',
+			'fieldtype': 'Date',
+			'width': 120
+		},
+		{
+			'label': _('Exit Interview'),
+			'fieldname': 'exit_interview',
+			'fieldtype': 'Link',
+			'options': 'Exit Interview',
+			'width': 150
+		},
+		{
+			'label': _('Interview Status'),
+			'fieldname': 'interview_status',
+			'fieldtype': 'Data',
+			'width': 130
+		},
+		{
+			'label': _('Final Decision'),
+			'fieldname': 'employee_status',
+			'fieldtype': 'Data',
+			'width': 150
+		},
+		{
+			'label': _('Full and Final Statement'),
+			'fieldname': 'full_and_final_statement',
+			'fieldtype': 'Link',
+			'options': 'Full and Final Statement',
+			'width': 180
+		},
+		{
+			'label': _('Department'),
+			'fieldname': 'department',
+			'fieldtype': 'Link',
+			'options': 'Department',
+			'width': 120
+		},
+		{
+			'label': _('Designation'),
+			'fieldname': 'designation',
+			'fieldtype': 'Link',
+			'options': 'Designation',
+			'width': 120
+		},
+		{
+			'label': _('Reports To'),
+			'fieldname': 'reports_to',
+			'fieldtype': 'Link',
+			'options': 'Employee',
+			'width': 120
+		}
+	]
+
+def get_data(filters):
+	employee = frappe.qb.DocType('Employee')
+	interview = frappe.qb.DocType('Exit Interview')
+	fnf = frappe.qb.DocType('Full and Final Statement')
+
+	query = (
+		frappe.qb.from_(employee)
+			.left_join(interview).on(interview.employee == employee.name)
+			.left_join(fnf).on(fnf.employee == employee.name)
+			.select(
+				employee.name.as_('employee'), employee.employee_name.as_('employee_name'),
+				employee.date_of_joining.as_('date_of_joining'), employee.relieving_date.as_('relieving_date'),
+				employee.department.as_('department'), employee.designation.as_('designation'),
+				employee.reports_to.as_('reports_to'), interview.name.as_('exit_interview'),
+				interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'),
+				interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement'))
+			.distinct()
+			.where(
+				((employee.relieving_date.isnotnull()) | (employee.relieving_date != ''))
+				& ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2)))
+				& ((fnf.name.isnull()) | ((fnf.name.isnotnull()) & (fnf.docstatus != 2)))
+			).orderby(employee.relieving_date, order=Order.asc)
+	)
+
+	query = get_conditions(filters, query, employee, interview, fnf)
+	result = query.run(as_dict=True)
+
+	return result
+
+
+def get_conditions(filters, query, employee, interview, fnf):
+	if filters.get('from_date') and filters.get('to_date'):
+		query = query.where(employee.relieving_date[getdate(filters.get('from_date')):getdate(filters.get('to_date'))])
+
+	elif filters.get('from_date'):
+		query = query.where(employee.relieving_date >= filters.get('from_date'))
+
+	elif filters.get('to_date'):
+		query = query.where(employee.relieving_date <= filters.get('to_date'))
+
+	if filters.get('company'):
+		query = query.where(employee.company == filters.get('company'))
+
+	if filters.get('department'):
+		query = query.where(employee.department == filters.get('department'))
+
+	if filters.get('designation'):
+		query = query.where(employee.designation == filters.get('designation'))
+
+	if filters.get('employee'):
+		query = query.where(employee.name == filters.get('employee'))
+
+	if filters.get('reports_to'):
+		query = query.where(employee.reports_to == filters.get('reports_to'))
+
+	if filters.get('interview_status'):
+		query = query.where(interview.status == filters.get('interview_status'))
+
+	if filters.get('final_decision'):
+		query = query.where(interview.employee_status == filters.get('final_decision'))
+
+	if filters.get('exit_interview_pending'):
+		query = query.where((interview.name == '') | (interview.name.isnull()))
+
+	if filters.get('questionnaire_pending'):
+		query = query.where((interview.reference_document_name == '') | (interview.reference_document_name.isnull()))
+
+	if filters.get('fnf_pending'):
+		query = query.where((fnf.name == '') | (fnf.name.isnull()))
+
+	return query
+
+
+def get_chart_data(data):
+	if not data:
+		return None
+
+	retained = 0
+	exit_confirmed = 0
+	pending = 0
+
+	for entry in data:
+		if entry.employee_status == 'Employee Retained':
+			retained += 1
+		elif entry.employee_status == 'Exit Confirmed':
+			exit_confirmed += 1
+		else:
+			pending += 1
+
+	chart = {
+		'data': {
+			'labels': [_('Retained'), _('Exit Confirmed'), _('Decision Pending')],
+			'datasets': [{'name': _('Employee Status'), 'values': [retained, exit_confirmed, pending]}]
+		},
+		'type': 'donut',
+		'colors': ['green', 'red', 'blue'],
+	}
+
+	return chart
+
+
+def get_report_summary(data):
+	if not data:
+		return None
+
+	total_resignations = len(data)
+	interviews_pending = len([entry.name for entry in data if not entry.exit_interview])
+	fnf_pending = len([entry.name for entry in data if not entry.full_and_final_statement])
+	questionnaires_pending = len([entry.name for entry in data if not entry.questionnaire])
+
+	return [
+		{
+			'value': total_resignations,
+			'label': _('Total Resignations'),
+			'indicator': 'Red' if total_resignations > 0 else 'Green',
+			'datatype': 'Int',
+		},
+		{
+			'value': interviews_pending,
+			'label': _('Pending Interviews'),
+			'indicator': 'Blue' if interviews_pending > 0 else 'Green',
+			'datatype': 'Int',
+		},
+		{
+			'value': fnf_pending,
+			'label': _('Pending FnF'),
+			'indicator': 'Blue' if fnf_pending > 0 else 'Green',
+			'datatype': 'Int',
+		},
+		{
+			'value': questionnaires_pending,
+			'label': _('Pending Questionnaires'),
+			'indicator': 'Blue' if questionnaires_pending > 0 else 'Green',
+			'datatype': 'Int'
+		},
+	]
+
diff --git a/erpnext/hr/report/employee_exits/test_employee_exits.py b/erpnext/hr/report/employee_exits/test_employee_exits.py
new file mode 100644
index 0000000..d7e95a6
--- /dev/null
+++ b/erpnext/hr/report/employee_exits/test_employee_exits.py
@@ -0,0 +1,242 @@
+import unittest
+
+import frappe
+from frappe.utils import add_days, getdate
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.exit_interview.test_exit_interview import create_exit_interview
+from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import (
+	create_full_and_final_statement,
+)
+from erpnext.hr.report.employee_exits.employee_exits import execute
+
+
+class TestEmployeeExits(unittest.TestCase):
+	@classmethod
+	def setUpClass(cls):
+		create_company()
+		frappe.db.sql("delete from `tabEmployee` where company='Test Company'")
+		frappe.db.sql("delete from `tabFull and Final Statement` where company='Test Company'")
+		frappe.db.sql("delete from `tabExit Interview` where company='Test Company'")
+
+		cls.create_records()
+
+	@classmethod
+	def tearDownClass(cls):
+		frappe.db.rollback()
+
+	@classmethod
+	def create_records(cls):
+		cls.emp1 = make_employee(
+			'employeeexit1@example.com',
+			company='Test Company',
+			date_of_joining=getdate('01-10-2021'),
+			relieving_date=add_days(getdate(), 14),
+			designation='Accountant'
+		)
+		cls.emp2 = make_employee(
+			'employeeexit2@example.com',
+			company='Test Company',
+			date_of_joining=getdate('01-12-2021'),
+			relieving_date=add_days(getdate(), 15),
+			designation='Accountant'
+		)
+
+		cls.emp3 = make_employee(
+			'employeeexit3@example.com',
+			company='Test Company',
+			date_of_joining=getdate('02-12-2021'),
+			relieving_date=add_days(getdate(), 29),
+			designation='Engineer'
+		)
+		cls.emp4 = make_employee(
+			'employeeexit4@example.com',
+			company='Test Company',
+			date_of_joining=getdate('01-12-2021'),
+			relieving_date=add_days(getdate(), 30),
+			designation='Engineer'
+		)
+
+		# exit interview for 3 employees only
+		cls.interview1 = create_exit_interview(cls.emp1)
+		cls.interview2 = create_exit_interview(cls.emp2)
+		cls.interview3 = create_exit_interview(cls.emp3)
+
+		# create fnf for some records
+		cls.fnf1 = create_full_and_final_statement(cls.emp1)
+		cls.fnf2 = create_full_and_final_statement(cls.emp2)
+
+		# link questionnaire for a few records
+		# setting employee doctype as reference instead of creating a questionnaire
+		# since this is just for a test
+		frappe.db.set_value('Exit Interview', cls.interview1.name, {
+			'ref_doctype': 'Employee',
+			'reference_document_name': cls.emp1
+		})
+
+		frappe.db.set_value('Exit Interview', cls.interview2.name, {
+			'ref_doctype': 'Employee',
+			'reference_document_name': cls.emp2
+		})
+
+		frappe.db.set_value('Exit Interview', cls.interview3.name, {
+			'ref_doctype': 'Employee',
+			'reference_document_name': cls.emp3
+		})
+
+
+	def test_employee_exits_summary(self):
+		filters = {
+			'company': 'Test Company',
+			'from_date': getdate(),
+			'to_date': add_days(getdate(), 15),
+			'designation': 'Accountant'
+		}
+
+		report = execute(filters)
+
+		employee1 = frappe.get_doc('Employee', self.emp1)
+		employee2 = frappe.get_doc('Employee', self.emp2)
+		expected_data = [
+			{
+				'employee': employee1.name,
+				'employee_name': employee1.employee_name,
+				'date_of_joining': employee1.date_of_joining,
+				'relieving_date': employee1.relieving_date,
+				'department': employee1.department,
+				'designation': employee1.designation,
+				'reports_to': None,
+				'exit_interview': self.interview1.name,
+				'interview_status': self.interview1.status,
+				'employee_status': '',
+				'questionnaire': employee1.name,
+				'full_and_final_statement': self.fnf1.name
+			},
+			{
+				'employee': employee2.name,
+				'employee_name': employee2.employee_name,
+				'date_of_joining': employee2.date_of_joining,
+				'relieving_date': employee2.relieving_date,
+				'department': employee2.department,
+				'designation': employee2.designation,
+				'reports_to': None,
+				'exit_interview': self.interview2.name,
+				'interview_status': self.interview2.status,
+				'employee_status': '',
+				'questionnaire': employee2.name,
+				'full_and_final_statement': self.fnf2.name
+			}
+		]
+
+		self.assertEqual(expected_data, report[1]) # rows
+
+
+	def test_pending_exit_interviews_summary(self):
+		filters = {
+			'company': 'Test Company',
+			'from_date': getdate(),
+			'to_date': add_days(getdate(), 30),
+			'exit_interview_pending': 1
+		}
+
+		report = execute(filters)
+
+		employee4 = frappe.get_doc('Employee', self.emp4)
+		expected_data = [{
+			'employee': employee4.name,
+			'employee_name': employee4.employee_name,
+			'date_of_joining': employee4.date_of_joining,
+			'relieving_date': employee4.relieving_date,
+			'department': employee4.department,
+			'designation': employee4.designation,
+			'reports_to': None,
+			'exit_interview': None,
+			'interview_status': None,
+			'employee_status': None,
+			'questionnaire': None,
+			'full_and_final_statement': None
+		}]
+
+		self.assertEqual(expected_data, report[1]) # rows
+
+	def test_pending_exit_questionnaire_summary(self):
+		filters = {
+			'company': 'Test Company',
+			'from_date': getdate(),
+			'to_date': add_days(getdate(), 30),
+			'questionnaire_pending': 1
+		}
+
+		report = execute(filters)
+
+		employee4 = frappe.get_doc('Employee', self.emp4)
+		expected_data = [{
+			'employee': employee4.name,
+			'employee_name': employee4.employee_name,
+			'date_of_joining': employee4.date_of_joining,
+			'relieving_date': employee4.relieving_date,
+			'department': employee4.department,
+			'designation': employee4.designation,
+			'reports_to': None,
+			'exit_interview': None,
+			'interview_status': None,
+			'employee_status': None,
+			'questionnaire': None,
+			'full_and_final_statement': None
+		}]
+
+		self.assertEqual(expected_data, report[1]) # rows
+
+
+	def test_pending_fnf_summary(self):
+		filters = {
+			'company': 'Test Company',
+			'fnf_pending': 1
+		}
+
+		report = execute(filters)
+
+		employee3 = frappe.get_doc('Employee', self.emp3)
+		employee4 = frappe.get_doc('Employee', self.emp4)
+		expected_data = [
+			{
+				'employee': employee3.name,
+				'employee_name': employee3.employee_name,
+				'date_of_joining': employee3.date_of_joining,
+				'relieving_date': employee3.relieving_date,
+				'department': employee3.department,
+				'designation': employee3.designation,
+				'reports_to': None,
+				'exit_interview': self.interview3.name,
+				'interview_status': self.interview3.status,
+				'employee_status': '',
+				'questionnaire': employee3.name,
+				'full_and_final_statement': None
+			},
+			{
+				'employee': employee4.name,
+				'employee_name': employee4.employee_name,
+				'date_of_joining': employee4.date_of_joining,
+				'relieving_date': employee4.relieving_date,
+				'department': employee4.department,
+				'designation': employee4.designation,
+				'reports_to': None,
+				'exit_interview': None,
+				'interview_status': None,
+				'employee_status': None,
+				'questionnaire': None,
+				'full_and_final_statement': None
+			}
+		]
+
+		self.assertEqual(expected_data, report[1]) # rows
+
+
+def create_company():
+	if not frappe.db.exists('Company', 'Test Company'):
+		frappe.get_doc({
+			'doctype': 'Company',
+			'company_name': 'Test Company',
+			'default_currency': 'INR',
+			'country': 'India'
+		}).insert()
\ No newline at end of file
diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json
index 7408d63..85e641c 100644
--- a/erpnext/hr/workspace/hr/hr.json
+++ b/erpnext/hr/workspace/hr/hr.json
@@ -5,7 +5,7 @@
    "label": "Outgoing Salary"
   }
  ],
- "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Human Resource\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Employee\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Leave Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Job Applicant\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee Lifecycle\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Shift Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Leaves\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Expense Claims\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fleet Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Recruitment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loans\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Training\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Performance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]",
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Human Resource\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Employee\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leave Application\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Attendance\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Applicant\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Monthly Attendance Sheet\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports &amp; Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Lifecycle\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Exit\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Shift Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Leaves\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Expense Claims\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loans\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Recruitment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Performance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fleet Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Training\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
  "creation": "2020-03-02 15:48:58.322521",
  "docstatus": 0,
  "doctype": "Workspace",
@@ -16,14 +16,6 @@
  "label": "HR",
  "links": [
   {
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Employee",
-   "link_count": 0,
-   "onboard": 0,
-   "type": "Card Break"
-  },
-  {
    "dependencies": "",
    "hidden": 0,
    "is_query_report": 0,
@@ -112,14 +104,6 @@
    "type": "Link"
   },
   {
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Employee Lifecycle",
-   "link_count": 0,
-   "onboard": 0,
-   "type": "Card Break"
-  },
-  {
    "dependencies": "Job Applicant",
    "hidden": 0,
    "is_query_report": 0,
@@ -228,14 +212,6 @@
    "type": "Link"
   },
   {
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Shift Management",
-   "link_count": 0,
-   "onboard": 0,
-   "type": "Card Break"
-  },
-  {
    "dependencies": "",
    "hidden": 0,
    "is_query_report": 0,
@@ -269,14 +245,6 @@
    "type": "Link"
   },
   {
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Leaves",
-   "link_count": 0,
-   "onboard": 0,
-   "type": "Card Break"
-  },
-  {
    "dependencies": "",
    "hidden": 0,
    "is_query_report": 0,
@@ -387,14 +355,6 @@
    "type": "Link"
   },
   {
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Attendance",
-   "link_count": 0,
-   "onboard": 0,
-   "type": "Card Break"
-  },
-  {
    "dependencies": "Employee",
    "hidden": 0,
    "is_query_report": 0,
@@ -450,14 +410,6 @@
    "type": "Link"
   },
   {
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Expense Claims",
-   "link_count": 0,
-   "onboard": 0,
-   "type": "Card Break"
-  },
-  {
    "dependencies": "Employee",
    "hidden": 0,
    "is_query_report": 0,
@@ -490,14 +442,6 @@
    "type": "Link"
   },
   {
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Settings",
-   "link_count": 0,
-   "onboard": 0,
-   "type": "Card Break"
-  },
-  {
    "dependencies": "",
    "hidden": 0,
    "is_query_report": 0,
@@ -533,14 +477,6 @@
   {
    "hidden": 0,
    "is_query_report": 0,
-   "label": "Fleet Management",
-   "link_count": 0,
-   "onboard": 0,
-   "type": "Card Break"
-  },
-  {
-   "hidden": 0,
-   "is_query_report": 0,
    "label": "Driver",
    "link_count": 0,
    "link_to": "Driver",
@@ -582,14 +518,6 @@
    "type": "Link"
   },
   {
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Recruitment",
-   "link_count": 0,
-   "onboard": 0,
-   "type": "Card Break"
-  },
-  {
    "dependencies": "",
    "hidden": 0,
    "is_query_report": 0,
@@ -809,14 +737,6 @@
    "type": "Link"
   },
   {
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Key Reports",
-   "link_count": 0,
-   "onboard": 0,
-   "type": "Card Break"
-  },
-  {
    "dependencies": "Attendance",
    "hidden": 0,
    "is_query_report": 1,
@@ -933,9 +853,796 @@
    "link_type": "Report",
    "onboard": 0,
    "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Lifecycle",
+   "link_count": 7,
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "dependencies": "Job Applicant",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Onboarding",
+   "link_count": 0,
+   "link_to": "Employee Onboarding",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Skill Map",
+   "link_count": 0,
+   "link_to": "Employee Skill Map",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Promotion",
+   "link_count": 0,
+   "link_to": "Employee Promotion",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Transfer",
+   "link_count": 0,
+   "link_to": "Employee Transfer",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Grievance Type",
+   "link_count": 0,
+   "link_to": "Grievance Type",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Grievance",
+   "link_count": 0,
+   "link_to": "Employee Grievance",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Onboarding Template",
+   "link_count": 0,
+   "link_to": "Employee Onboarding Template",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Exit",
+   "link_count": 4,
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Separation Template",
+   "link_count": 0,
+   "link_to": "Employee Separation Template",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Separation",
+   "link_count": 0,
+   "link_to": "Employee Separation",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Full and Final Statement",
+   "link_count": 0,
+   "link_to": "Full and Final Statement",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Exit Interview",
+   "link_count": 0,
+   "link_to": "Exit Interview",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee",
+   "link_count": 8,
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee",
+   "link_count": 0,
+   "link_to": "Employee",
+   "link_type": "DocType",
+   "onboard": 1,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employment Type",
+   "link_count": 0,
+   "link_to": "Employment Type",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Branch",
+   "link_count": 0,
+   "link_to": "Branch",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Department",
+   "link_count": 0,
+   "link_to": "Department",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Designation",
+   "link_count": 0,
+   "link_to": "Designation",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Grade",
+   "link_count": 0,
+   "link_to": "Employee Grade",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Group",
+   "link_count": 0,
+   "link_to": "Employee Group",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Health Insurance",
+   "link_count": 0,
+   "link_to": "Employee Health Insurance",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Key Reports",
+   "link_count": 7,
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "dependencies": "Attendance",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Monthly Attendance Sheet",
+   "link_count": 0,
+   "link_to": "Monthly Attendance Sheet",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Staffing Plan",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Recruitment Analytics",
+   "link_count": 0,
+   "link_to": "Recruitment Analytics",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Employee Analytics",
+   "link_count": 0,
+   "link_to": "Employee Analytics",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Employee Leave Balance",
+   "link_count": 0,
+   "link_to": "Employee Leave Balance",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Employee Leave Balance Summary",
+   "link_count": 0,
+   "link_to": "Employee Leave Balance Summary",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee Advance",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Employee Advance Summary",
+   "link_count": 0,
+   "link_to": "Employee Advance Summary",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Exits",
+   "link_count": 0,
+   "link_to": "Employee Exits",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Recruitment",
+   "link_count": 11,
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Job Opening",
+   "link_count": 0,
+   "link_to": "Job Opening",
+   "link_type": "DocType",
+   "onboard": 1,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Referral",
+   "link_count": 0,
+   "link_to": "Employee Referral",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Job Applicant",
+   "link_count": 0,
+   "link_to": "Job Applicant",
+   "link_type": "DocType",
+   "onboard": 1,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Job Offer",
+   "link_count": 0,
+   "link_to": "Job Offer",
+   "link_type": "DocType",
+   "onboard": 1,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Staffing Plan",
+   "link_count": 0,
+   "link_to": "Staffing Plan",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Appointment Letter",
+   "link_count": 0,
+   "link_to": "Appointment Letter",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Appointment Letter Template",
+   "link_count": 0,
+   "link_to": "Appointment Letter Template",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Interview Type",
+   "link_count": 0,
+   "link_to": "Interview Type",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Interview Round",
+   "link_count": 0,
+   "link_to": "Interview Round",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Interview",
+   "link_count": 0,
+   "link_to": "Interview",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Interview Feedback",
+   "link_count": 0,
+   "link_to": "Interview Feedback",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Fleet Management",
+   "link_count": 4,
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Driver",
+   "link_count": 0,
+   "link_to": "Driver",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Vehicle",
+   "link_count": 0,
+   "link_to": "Vehicle",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Vehicle Log",
+   "link_count": 0,
+   "link_to": "Vehicle Log",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Vehicle",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Vehicle Expenses",
+   "link_count": 0,
+   "link_to": "Vehicle Expenses",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Settings",
+   "link_count": 3,
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "HR Settings",
+   "link_count": 0,
+   "link_to": "HR Settings",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Daily Work Summary Group",
+   "link_count": 0,
+   "link_to": "Daily Work Summary Group",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Team Updates",
+   "link_count": 0,
+   "link_to": "team-updates",
+   "link_type": "Page",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Expense Claims",
+   "link_count": 3,
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Expense Claim",
+   "link_count": 0,
+   "link_to": "Expense Claim",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Advance",
+   "link_count": 0,
+   "link_to": "Employee Advance",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Travel Request",
+   "link_count": 0,
+   "link_to": "Travel Request",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Attendance",
+   "link_count": 5,
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Attendance Tool",
+   "link_count": 0,
+   "link_to": "Employee Attendance Tool",
+   "link_type": "DocType",
+   "onboard": 1,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Attendance",
+   "link_count": 0,
+   "link_to": "Attendance",
+   "link_type": "DocType",
+   "onboard": 1,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Attendance Request",
+   "link_count": 0,
+   "link_to": "Attendance Request",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Upload Attendance",
+   "link_count": 0,
+   "link_to": "Upload Attendance",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Employee Checkin",
+   "link_count": 0,
+   "link_to": "Employee Checkin",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Leaves",
+   "link_count": 10,
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Holiday List",
+   "link_count": 0,
+   "link_to": "Holiday List",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Leave Type",
+   "link_count": 0,
+   "link_to": "Leave Type",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Leave Period",
+   "link_count": 0,
+   "link_to": "Leave Period",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Leave Type",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Leave Policy",
+   "link_count": 0,
+   "link_to": "Leave Policy",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Leave Policy",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Leave Policy Assignment",
+   "link_count": 0,
+   "link_to": "Leave Policy Assignment",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Leave Application",
+   "link_count": 0,
+   "link_to": "Leave Application",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Leave Allocation",
+   "link_count": 0,
+   "link_to": "Leave Allocation",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Leave Encashment",
+   "link_count": 0,
+   "link_to": "Leave Encashment",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Leave Block List",
+   "link_count": 0,
+   "link_to": "Leave Block List",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "Employee",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Compensatory Leave Request",
+   "link_count": 0,
+   "link_to": "Compensatory Leave Request",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Shift Management",
+   "link_count": 3,
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Shift Type",
+   "link_count": 0,
+   "link_to": "Shift Type",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Shift Request",
+   "link_count": 0,
+   "link_to": "Shift Request",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "dependencies": "",
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Shift Assignment",
+   "link_count": 0,
+   "link_to": "Shift Assignment",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Link"
   }
  ],
- "modified": "2021-08-31 12:18:59.842919",
+ "modified": "2021-12-05 22:05:13.004462",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "HR",
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
index 37ea3fd..5017126 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
@@ -56,9 +56,14 @@
 
 		ms.submit()
 		s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1])
-		test = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
+
+		# Check if item is mapped in visit.
+		test_map_visit = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id)
+		self.assertEqual(len(test_map_visit.purposes), 1)
+		self.assertEqual(test_map_visit.purposes[0].item_name, "_Test Item")
+
 		visit = frappe.new_doc('Maintenance Visit')
-		visit = test
+		visit = test_map_visit
 		visit.maintenance_schedule = ms.name
 		visit.maintenance_schedule_detail = s_id
 		visit.completion_status = "Partially Completed"
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
index 7f98354..d2197a6 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
@@ -43,14 +43,11 @@
 				}
 			});
 		}
-		else {
-			frm.clear_table("purposes");
-		}
-
 		if (!frm.doc.status) {
 			frm.set_value({ status: 'Draft' });
 		}
 		if (frm.doc.__islocal) {
+			frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes");
 			frm.set_value({ mntc_date: frappe.datetime.get_today() });
 		}
 	},
diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
index d5db3fc..eff2344 100644
--- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py
@@ -1,17 +1,15 @@
 # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
-
-import unittest
-
 import frappe
 from frappe.utils import add_months, today
 
 from erpnext import get_company_currency
+from erpnext.tests.utils import ERPNextTestCase
 
 from .blanket_order import make_order
 
 
-class TestBlanketOrder(unittest.TestCase):
+class TestBlanketOrder(ERPNextTestCase):
 	def setUp(self):
 		frappe.flags.args = frappe._dict()
 
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 2cd8f8c..f82d9a0 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -922,7 +922,7 @@
 				rm_item_exists = True
 		if bom.item.lower() == item.lower() or \
 			bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower():
- 				rm_item_exists = True
+				rm_item_exists = True
 		if not rm_item_exists:
 			frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))
 
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 4c03230..178d92c 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -2,7 +2,6 @@
 # License: GNU General Public License v3. See license.txt
 
 
-import unittest
 from collections import deque
 from functools import partial
 
@@ -18,10 +17,11 @@
 	create_stock_reconciliation,
 )
 from erpnext.tests.test_subcontracting import set_backflush_based_on
+from erpnext.tests.utils import ERPNextTestCase
 
 test_records = frappe.get_test_records('BOM')
 
-class TestBOM(unittest.TestCase):
+class TestBOM(ERPNextTestCase):
 	def setUp(self):
 		if not frappe.get_value('Item', '_Test Item'):
 			make_test_records('Item')
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index ec617f3..c7be7ef 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -11,6 +11,7 @@
   "col_break1",
   "workstation",
   "time_in_mins",
+  "fixed_time",
   "costing_section",
   "hour_rate",
   "base_hour_rate",
@@ -80,6 +81,14 @@
    "reqd": 1
   },
   {
+   "default": "0",
+   "description": "Operation time does not depend on quantity to produce",
+   "fieldname": "fixed_time",
+   "fieldtype": "Check",
+   "in_list_view": 1,
+   "label": "Fixed Time"
+  },
+  {
    "fieldname": "operating_cost",
    "fieldtype": "Currency",
    "in_list_view": 1,
@@ -177,12 +186,13 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-09-13 16:45:01.092868",
+ "modified": "2021-12-15 03:00:00.473173",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "BOM Operation",
  "owner": "Administrator",
  "permissions": [],
  "sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
 }
\ No newline at end of file
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 526c243..12576cb 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
@@ -1,19 +1,16 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
-
-
-import unittest
-
 import frappe
 
 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
+from erpnext.tests.utils import ERPNextTestCase
 
 test_records = frappe.get_test_records('BOM')
 
-class TestBOMUpdateTool(unittest.TestCase):
+class TestBOMUpdateTool(ERPNextTestCase):
 	def test_replace_bom(self):
 		current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
 
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 453ad50..d85b8a6 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -75,6 +75,32 @@
 			&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
 			frm.trigger("prepare_timer_buttons");
 		}
+
+		frm.trigger("setup_quality_inspection");
+		if (frm.doc.work_order) {
+			frappe.db.get_value('Work Order', frm.doc.work_order,
+				'transfer_material_against').then((r) => {
+				if (r.message.transfer_material_against == 'Work Order') {
+					frm.set_df_property('items', 'hidden', 1);
+				}
+			});
+		}
+	},
+
+	setup_quality_inspection: function(frm) {
+		let quality_inspection_field = frm.get_docfield("quality_inspection");
+		quality_inspection_field.get_route_options_for_new_doc = function(frm) {
+			return  {
+				"inspection_type": "In Process",
+				"reference_type": "Job Card",
+				"reference_name": frm.doc.name,
+				"item_code": frm.doc.production_item,
+				"item_name": frm.doc.item_name,
+				"item_serial_no": frm.doc.serial_no,
+				"batch_no": frm.doc.batch_no,
+				"quality_inspection_template": frm.doc.quality_inspection_template,
+			};
+		};
 	},
 
 	setup_corrective_job_card: function(frm) {
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index 6528199..5a071f1 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -19,6 +19,7 @@
   "serial_no",
   "column_break_12",
   "wip_warehouse",
+  "quality_inspection_template",
   "quality_inspection",
   "project",
   "batch_no",
@@ -408,11 +409,18 @@
    "no_copy": 1,
    "options": "Job Card Scrap Item",
    "print_hide": 1
+  },
+  {
+   "fetch_from": "operation.quality_inspection_template",
+   "fieldname": "quality_inspection_template",
+   "fieldtype": "Link",
+   "label": "Quality Inspection Template",
+   "options": "Quality Inspection Template"
   }
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2021-11-12 10:15:03.572401",
+ "modified": "2021-11-24 19:17:40.879235",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Job Card",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
index acaa895..2c48872 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py
@@ -4,10 +4,17 @@
 def get_data():
 	return {
 		'fieldname': 'job_card',
+		'non_standard_fieldnames': {
+			'Quality Inspection': 'reference_name'
+		},
 		'transactions': [
 			{
 				'label': _('Transactions'),
 				'items': ['Material Request', 'Stock Entry']
+			},
+			{
+				'label': _('Reference'),
+				'items': ['Quality Inspection']
 			}
 		]
 	}
diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py
index 9b4fc8b..bb5004b 100644
--- a/erpnext/manufacturing/doctype/job_card/test_job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py
@@ -1,6 +1,5 @@
 # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
-import unittest
 
 import frappe
 from frappe.utils import random_string
@@ -12,9 +11,10 @@
 from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
 from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
 from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestJobCard(unittest.TestCase):
+class TestJobCard(ERPNextTestCase):
 	def setUp(self):
 		make_bom_for_jc_tests()
 
@@ -329,4 +329,4 @@
 	bom.rm_cost_as_per = "Valuation Rate"
 	bom.items[0].uom = "_Test UOM 1"
 	bom.items[0].conversion_factor = 5
-	bom.insert()
\ No newline at end of file
+	bom.insert()
diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json
index 10a97ed..753552c 100644
--- a/erpnext/manufacturing/doctype/operation/operation.json
+++ b/erpnext/manufacturing/doctype/operation/operation.json
@@ -13,6 +13,7 @@
   "is_corrective_operation",
   "job_card_section",
   "create_job_card_based_on_batch_size",
+  "quality_inspection_template",
   "column_break_6",
   "batch_size",
   "sub_operations_section",
@@ -92,15 +93,22 @@
    "fieldname": "is_corrective_operation",
    "fieldtype": "Check",
    "label": "Is Corrective Operation"
+  },
+  {
+   "fieldname": "quality_inspection_template",
+   "fieldtype": "Link",
+   "label": "Quality Inspection Template",
+   "options": "Quality Inspection Template"
   }
  ],
  "icon": "fa fa-wrench",
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2021-01-12 15:09:23.593338",
+ "modified": "2021-11-24 19:15:24.357187",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Operation",
+ "naming_rule": "Set by user",
  "owner": "Administrator",
  "permissions": [
   {
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index a2980a7..2febc1e 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1,8 +1,5 @@
 # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
-
-import unittest
-
 import frappe
 from frappe.utils import add_to_date, flt, now_datetime, nowdate
 
@@ -17,9 +14,10 @@
 from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
 	create_stock_reconciliation,
 )
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestProductionPlan(unittest.TestCase):
+class TestProductionPlan(ERPNextTestCase):
 	def setUp(self):
 		for item in ['Test Production Item 1', 'Subassembly Item 1',
 			'Raw Material Item 1', 'Raw Material Item 2']:
diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py
index 68d9dec..e90b0a7 100644
--- a/erpnext/manufacturing/doctype/routing/test_routing.py
+++ b/erpnext/manufacturing/doctype/routing/test_routing.py
@@ -1,17 +1,15 @@
 # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
-
-import unittest
-
 import frappe
 from frappe.test_runner import make_test_records
 
 from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
 from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
 from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestRouting(unittest.TestCase):
+class TestRouting(ERPNextTestCase):
 	@classmethod
 	def setUpClass(cls):
 		cls.item_code = "Test Routing Item - A"
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index f590d68..86c687f 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -1,6 +1,5 @@
 # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
-import unittest
 
 import frappe
 from frappe.utils import add_months, cint, flt, now, today
@@ -29,6 +28,9 @@
 		self.warehouse = '_Test Warehouse 2 - _TC'
 		self.item = '_Test Item'
 
+	def tearDown(self):
+		frappe.db.rollback()
+
 	def check_planned_qty(self):
 
 		planned0 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item",
@@ -92,7 +94,7 @@
 
 	def test_reserved_qty_for_partial_completion(self):
 		item = "_Test Item"
-		warehouse = create_warehouse("Test Warehouse for reserved_qty - _TC")
+		warehouse = "_Test Warehouse - _TC"
 
 		bin1_at_start = get_bin(item, warehouse)
 
@@ -844,6 +846,45 @@
 			close_work_order(wo_order, "Closed")
 			self.assertEqual(wo_order.get('status'), "Closed")
 
+	def test_fix_time_operations(self):
+		bom = frappe.get_doc({
+			"doctype": "BOM",
+			"item": "_Test FG Item 2",
+			"is_active": 1,
+			"is_default": 1,
+			"quantity": 1.0,
+			"with_operations": 1,
+			"operations": [
+				{
+					"operation": "_Test Operation 1",
+					"description": "_Test",
+					"workstation": "_Test Workstation 1",
+					"time_in_mins": 60,
+					"operating_cost": 140,
+					"fixed_time": 1
+				}
+			],
+			"items": [
+				{
+					"amount": 5000.0,
+					"doctype": "BOM Item",
+					"item_code": "_Test Item",
+					"parentfield": "items",
+					"qty": 1.0,
+					"rate": 5000.0,
+				},
+			],
+		})
+		bom.save()
+		bom.submit()
+
+
+		wo1 = make_wo_order_test_record(item=bom.item, bom_no=bom.name, qty=1, skip_transfer=1, do_not_submit=1)
+		wo2 = make_wo_order_test_record(item=bom.item, bom_no=bom.name, qty=2, skip_transfer=1, do_not_submit=1)
+
+		self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins)
+
+
 def update_job_card(job_card):
 	job_card_doc = frappe.get_doc('Job Card', job_card)
 	job_card_doc.set('scrap_items', [
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 0090f4d..170454c 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -505,16 +505,19 @@
 		"""Fetch operations from BOM and set in 'Work Order'"""
 
 		def _get_operations(bom_no, qty=1):
-			return frappe.db.sql(
-					f"""select
-						operation, description, workstation, idx,
-						base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins,
-						"Pending" as status, parent as bom, batch_size, sequence_id
-					from
-						`tabBOM Operation`
-					where
-						parent = %s order by idx
-					""", bom_no, as_dict=1)
+			data = frappe.get_all("BOM Operation",
+					filters={"parent": bom_no},
+					fields=["operation", "description", "workstation", "idx",
+						"base_hour_rate as hour_rate", "time_in_mins", "parent as bom",
+						"batch_size", "sequence_id", "fixed_time"],
+					order_by="idx")
+
+			for d in data:
+				if not d.fixed_time:
+					d.time_in_mins = flt(d.time_in_mins) * flt(qty)
+				d.status = "Pending"
+
+			return data
 
 
 		self.set('operations', [])
@@ -542,7 +545,8 @@
 
 	def calculate_time(self):
 		for d in self.get("operations"):
-			d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
+			if not d.fixed_time:
+				d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
 
 		self.calculate_operating_cost()
 
diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py
index 5ed5153..c298c0a 100644
--- a/erpnext/manufacturing/doctype/workstation/test_workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py
@@ -1,8 +1,5 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
 # See license.txt
-
-import unittest
-
 import frappe
 from frappe.test_runner import make_test_records
 
@@ -13,12 +10,13 @@
 	WorkstationHolidayError,
 	check_if_within_operating_hours,
 )
+from erpnext.tests.utils import ERPNextTestCase
 
 test_dependencies = ["Warehouse"]
 test_records = frappe.get_test_records('Workstation')
 make_test_records('Workstation')
 
-class TestWorkstation(unittest.TestCase):
+class TestWorkstation(ERPNextTestCase):
 	def test_validate_timings(self):
 		check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
 		check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index ee9060b..d9cedab 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -165,6 +165,7 @@
 erpnext.patches.v12_0.set_default_payroll_based_on
 erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse
 erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign
+erpnext.patches.v13_0.validate_options_for_data_field
 erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
 erpnext.patches.v12_0.fix_quotation_expired_status
 erpnext.patches.v12_0.rename_pos_closing_doctype
@@ -287,7 +288,6 @@
 erpnext.patches.v14_0.delete_einvoicing_doctypes
 erpnext.patches.v13_0.custom_fields_for_taxjar_integration          #08-11-2021
 erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
-erpnext.patches.v13_0.validate_options_for_data_field
 erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
 erpnext.patches.v14_0.delete_shopify_doctypes
 erpnext.patches.v13_0.fix_invoice_statuses
@@ -313,4 +313,8 @@
 erpnext.patches.v13_0.create_pan_field_for_india #2
 erpnext.patches.v14_0.delete_hub_doctypes
 erpnext.patches.v13_0.create_ksa_vat_custom_fields
+erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
 erpnext.patches.v14_0.migrate_crm_settings
+erpnext.patches.v13_0.rename_ksa_qr_field
+erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
+erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
new file mode 100644
index 0000000..aa2a2d3
--- /dev/null
+++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2020, Wahni Green Technologies and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+
+from erpnext.regional.saudi_arabia.setup import add_print_formats
+
+
+def execute():
+	company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
+	if company:
+		add_print_formats()
+		return
+
+	if frappe.db.exists('DocType', 'Print Format'):
+		frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
+		frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
+		for d in ('KSA VAT Invoice', 'KSA POS Invoice'):
+			frappe.db.set_value("Print Format", d, "disabled", 1)
diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py
new file mode 100644
index 0000000..f4f9b17
--- /dev/null
+++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2020, Wahni Green Technologies and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from frappe.model.utils.rename_field import rename_field
+
+
+def execute():
+	company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
+	if not company:
+		return
+
+	if frappe.db.exists('DocType', 'Sales Invoice'):
+		frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True)
+
+		# rename_field method assumes that the field already exists or the doc is synced
+		if not frappe.db.has_column('Sales Invoice', 'ksa_einv_qr'):
+			create_custom_fields({
+				'Sales Invoice': [
+					dict(
+						fieldname='ksa_einv_qr',
+						label='KSA E-Invoicing QR',
+						fieldtype='Attach Image',
+						read_only=1, no_copy=1, hidden=1
+					)
+				]
+			})
+
+		if frappe.db.has_column('Sales Invoice', 'qr_code'):
+			rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr')
+			frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code")
diff --git a/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py
new file mode 100644
index 0000000..8b1752b
--- /dev/null
+++ b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py
@@ -0,0 +1,27 @@
+import os
+
+import frappe
+from frappe import _
+
+
+def execute():
+	frappe.reload_doc("email", "doctype", "email_template")
+	frappe.reload_doc("hr", "doctype", "hr_settings")
+
+	template = frappe.db.exists("Email Template", _("Exit Questionnaire Notification"))
+	if not template:
+		base_path = frappe.get_app_path("erpnext", "hr", "doctype")
+		response = frappe.read_file(os.path.join(base_path, "exit_interview/exit_questionnaire_notification_template.html"))
+
+		template = frappe.get_doc({
+			"doctype": "Email Template",
+			"name": _("Exit Questionnaire Notification"),
+			"response": response,
+			"subject": _("Exit Questionnaire Notification"),
+			"owner": frappe.session.user,
+		}).insert(ignore_permissions=True)
+		template = template.name
+
+	hr_settings = frappe.get_doc("HR Settings")
+	hr_settings.exit_questionnaire_notification_template = template
+	hr_settings.save()
diff --git a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py
new file mode 100644
index 0000000..1cc5f38
--- /dev/null
+++ b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py
@@ -0,0 +1,27 @@
+import frappe
+
+
+def execute():
+	active_sla_documents = [sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])]
+
+	for doctype in active_sla_documents:
+		doctype = frappe.qb.DocType(doctype)
+		try:
+			frappe.qb.update(
+				doctype
+			).set(
+				doctype.agreement_status, 'First Response Due'
+			).where(
+				doctype.first_responded_on.isnull()
+			).run()
+
+			frappe.qb.update(
+				doctype
+			).set(
+				doctype.agreement_status, 'Resolution Due'
+			).where(
+				doctype.agreement_status == 'Ongoing'
+			).run()
+
+		except Exception:
+			frappe.log_error(title='Failed to Patch SLA Status')
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 05af09e..b035292 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -940,10 +940,12 @@
 
 	def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
 		amount, additional_amount = row.amount, row.additional_amount
+		timesheet_component = frappe.db.get_value("Salary Structure", self.salary_structure, "salary_component")
+
 		if (self.salary_structure and
 			cint(row.depends_on_payment_days) and cint(self.total_working_days)
 			and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary
-			and (not self.salary_slip_based_on_timesheet or
+			and (row.salary_component != timesheet_component or
 				getdate(self.start_date) < joining_date or
 				(relieving_date and getdate(self.end_date) > relieving_date)
 			)):
@@ -952,7 +954,7 @@
 			amount = flt((flt(row.default_amount) * flt(self.payment_days)
 				/ cint(self.total_working_days)), row.precision("amount")) + additional_amount
 
-		elif not self.payment_days and not self.salary_slip_based_on_timesheet and cint(row.depends_on_payment_days):
+		elif not self.payment_days and row.salary_component != timesheet_component and cint(row.depends_on_payment_days):
 			amount, additional_amount = 0, 0
 		elif not row.amount:
 			amount = flt(row.default_amount) + flt(row.additional_amount)
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index e4618c3..3052a2b 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -134,6 +134,57 @@
 
 		frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
 
+	def test_payment_days_in_salary_slip_based_on_timesheet(self):
+		from erpnext.hr.doctype.attendance.attendance import mark_attendance
+		from erpnext.projects.doctype.timesheet.test_timesheet import (
+			make_salary_structure_for_timesheet,
+			make_timesheet,
+		)
+		from erpnext.projects.doctype.timesheet.timesheet import (
+			make_salary_slip as make_salary_slip_for_timesheet,
+		)
+
+		# Payroll based on attendance
+		frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
+
+		emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company")
+		frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
+
+		# mark attendance
+		month_start_date = get_first_day(nowdate())
+		month_end_date = get_last_day(nowdate())
+
+		first_sunday = frappe.db.sql("""
+			select holiday_date from `tabHoliday`
+			where parent = 'Salary Slip Test Holiday List'
+				and holiday_date between %s and %s
+			order by holiday_date
+		""", (month_start_date, month_end_date))[0][0]
+
+		mark_attendance(emp, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent
+
+		# salary structure based on timesheet
+		make_salary_structure_for_timesheet(emp)
+		timesheet = make_timesheet(emp, simulate=True, is_billable=1)
+		salary_slip = make_salary_slip_for_timesheet(timesheet.name)
+		salary_slip.start_date = month_start_date
+		salary_slip.end_date = month_end_date
+		salary_slip.save()
+		salary_slip.submit()
+
+		no_of_days = self.get_no_of_days()
+		days_in_month = no_of_days[0]
+		no_of_holidays = no_of_days[1]
+
+		self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1)
+
+		# gross pay calculation based on attendance (payment days)
+		gross_pay = 78100 - ((78000 / (days_in_month - no_of_holidays)) * flt(salary_slip.leave_without_pay + salary_slip.absent_days))
+
+		self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
+
+		frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
+
 	def test_component_amount_dependent_on_another_payment_days_based_component(self):
 		from erpnext.hr.doctype.attendance.attendance import mark_attendance
 		from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py
index d59cc01..148d8ba 100644
--- a/erpnext/projects/doctype/timesheet/test_timesheet.py
+++ b/erpnext/projects/doctype/timesheet/test_timesheet.py
@@ -34,10 +34,6 @@
 		for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
 			frappe.db.sql("delete from `tab%s`" % dt)
 
-		if not frappe.db.exists("Salary Component", "Timesheet Component"):
-			frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
-
-
 	def test_timesheet_billing_amount(self):
 		emp = make_employee("test_employee_6@salary.com")
 
@@ -160,6 +156,9 @@
 	salary_structure_name = "Timesheet Salary Structure Test"
 	frequency = "Monthly"
 
+	if not frappe.db.exists("Salary Component", "Timesheet Component"):
+		frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
+
 	salary_structure = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True)
 	salary_structure.salary_component = "Timesheet Component"
 	salary_structure.salary_slip_based_on_timesheet = 1
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index f0facdd..9339c5d 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -84,6 +84,10 @@
 		});
 	},
 
+	route_to_pending_reposts: (args) => {
+		frappe.set_route('List', 'Repost Item Valuation', args);
+	},
+
 	proceed_save_with_reminders_frequency_change: () => {
 		frappe.ui.hide_open_dialog();
 
@@ -426,12 +430,9 @@
 					qty = row.qty;
 				}
 				row[item_field] = d.alternate_item;
-				frm.script_manager.trigger(item_field, row.doctype, row.name)
-					.then(() => {
-						frappe.model.set_value(row.doctype, row.name, 'qty', qty);
-						frappe.model.set_value(row.doctype, row.name,
-							opts.original_item_field, d.item_code);
-					});
+				frappe.model.set_value(row.doctype, row.name, 'qty', qty);
+				frappe.model.set_value(row.doctype, row.name, opts.original_item_field, d.item_code);
+				frm.trigger(item_field, row.doctype, row.name);
 			});
 
 			refresh_field(opts.child_docname);
@@ -831,7 +832,7 @@
 
 					refresh: function(frm) {
 						if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement
-							&& frm.doc.agreement_status === 'Ongoing') {
+							&& ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) {
 							frappe.call({
 								'method': 'frappe.client.get',
 								args: {
@@ -884,9 +885,11 @@
 function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
 	frm.dashboard.clear_headline();
 
-	let time_to_respond = get_status(frm.doc.response_by_variance);
-	if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') {
+	let time_to_respond;
+	if (!frm.doc.first_responded_on) {
 		time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
+	} else {
+		time_to_respond = get_status(frm.doc.response_by, frm.doc.first_responded_on);
 	}
 
 	let alert = `
@@ -899,9 +902,11 @@
 
 
 	if (apply_sla_for_resolution) {
-		let time_to_resolve = get_status(frm.doc.resolution_by_variance);
-		if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') {
+		let time_to_resolve;
+		if (!frm.doc.resolution_date) {
 			time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
+		} else {
+			time_to_resolve = get_status(frm.doc.resolution_by, frm.doc.resolution_date);
 		}
 
 		alert += `
@@ -924,8 +929,9 @@
 	return {'diff_display': diff_display, 'indicator': indicator};
 }
 
-function get_status(variance) {
-	if (variance > 0) {
+function get_status(expected, actual) {
+	const time_left = moment(expected).diff(moment(actual));
+	if (time_left >= 0) {
 		return {'diff_display': 'Fulfilled', 'indicator': 'green'};
 	} else {
 		return {'diff_display': 'Failed', 'indicator': 'red'};
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 9743c3b..fd3ec3c 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -213,7 +213,7 @@
 		tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
 
 	if tax_template_by_category:
-		party_details.get['taxes_and_charges'] = tax_template_by_category
+		party_details['taxes_and_charges'] = tax_template_by_category
 		return
 
 	if not party_details.place_of_supply: return party_details
diff --git a/erpnext/regional/print_format/ksa_pos_invoice/__init__.py b/erpnext/regional/print_format/ksa_pos_invoice/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/regional/print_format/ksa_pos_invoice/__init__.py
diff --git a/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json
new file mode 100644
index 0000000..c2a3092
--- /dev/null
+++ b/erpnext/regional/print_format/ksa_pos_invoice/ksa_pos_invoice.json
@@ -0,0 +1,32 @@
+{
+ "absolute_value": 0,
+ "align_labels_right": 0,
+ "creation": "2021-12-07 13:25:05.424827",
+ "css": "",
+ "custom_format": 1,
+ "default_print_language": "en",
+ "disabled": 1,
+ "doc_type": "POS Invoice",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font_size": 0,
+ "html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n    {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t<img src={{doc.ksa_einv_qr}}>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Cashier\") }}:</b> {{ doc.owner }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Time\") }}:</b> {{  doc.get_formatted(\"posting_time\") }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"40%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"35%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"net_amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t    {% if '%' in row.description %}\n\t\t\t\t\t    {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t    {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
+ "idx": 0,
+ "line_breaks": 0,
+ "margin_bottom": 0.0,
+ "margin_left": 0.0,
+ "margin_right": 0.0,
+ "margin_top": 0.0,
+ "modified": "2021-12-08 10:25:01.930885",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "KSA POS Invoice",
+ "owner": "Administrator",
+ "page_number": "Hide",
+ "print_format_builder": 0,
+ "print_format_builder_beta": 0,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 0,
+ "standard": "Yes"
+}
\ No newline at end of file
diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
index 8e9a728..6b64d47 100644
--- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
+++ b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
@@ -5,19 +5,19 @@
  "css": ".qr-code{\n    float:right;\n}\n\n.invoice-heading {\n  margin: 0;\n}\n\n.ksa-invoice-table {\n  border: 1px solid #888a8e;\n  border-collapse: collapse;\n  width: 100%;\n  margin: 20px 0;\n  font-size: 16px;\n}\n\n.ksa-invoice-table.two-columns td:nth-child(2) {\n  direction: rtl;\n}\n\n.ksa-invoice-table th {\n  border: 1px solid #888a8e;\n  max-width: 50%;\n  padding: 8px;\n}\n\n.ksa-invoice-table td {\n  padding: 5px;\n  border: 1px solid #888a8e;\n  max-width: 50%;\n}\n\n.ksa-invoice-table thead,\n.ksa-invoice-table tfoot {\n  text-transform: uppercase;\n}\n\n.qr-rtl {\n  direction: rtl;\n}\n\n.qr-flex{\n    display: flex;\n    justify-content: space-between;\n}",
  "custom_format": 1,
  "default_print_language": "en",
- "disabled": 0,
+ "disabled": 1,
  "doc_type": "Sales Invoice",
  "docstatus": 0,
  "doctype": "Print Format",
  "font_size": 14,
- "html": "<div class=\"ksa-vat-format\">\n    <div class=\"qr-flex\">\n        <div style=\"qr-flex: 1\">\n            <h2 class=\"invoice-heading\">TAX INVOICE</h2>\n            <h2 class=\"invoice-heading\">\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629</h2>\n        </div>\n        \n        <img class=\"qr-code\" src={{doc.qr_code}}>\n    </div>\n    {% set company = frappe.get_doc(\"Company\", doc.company)%}\n    {% if (doc.company_address) %}\n        {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n    {% endif %}\n    \n    {% if(doc.customer_address) %}\n        {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n    {% endif %}\n    \n    {% if(doc.shipping_address_name) %}\n        {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n    {% endif %}  \n        \n    <table class=\"ksa-invoice-table two-columns\">\n      <thead>\n        <tr>\n          <th>{{ company.name }}</th>\n          <th style=\"text-align: right;\">{{ company.company_name_in_arabic }}</th>\n        </tr>\n      </thead>\n\n      <tbody>\n        <!-- Invoice Info -->\n        <tr>\n          <td>Invoice#: {{doc.name}}</td>\n          <td>\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}</td>\n        </tr>\n        <tr>\n          <td>Invoice Date: {{doc.posting_date}}</td>\n          <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}</td>\n        </tr>\n        <tr>\n          <td>Date of Supply:{{doc.posting_date}}</td>\n          <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}</td>\n        </tr>\n        \n        <!--Supplier Info -->\n        <tr>\n          <td>Supplier:</td>\n          <td>\u0627\u0644\u0645\u0648\u0631\u062f:</td>\n        </tr>\n\t\t{% if (company.tax_id) %}\n        <tr>\n          <td>Supplier Tax Identification Number:</td>\n          <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:</td>\n        </tr>\n        <tr>\n          <td>{{ company.tax_id }}</td>\n          <td>{{ company.tax_id }}</td>\n        </tr>\n        {% endif %}\n        <tr>\n          <td>{{ company.name }}</td>\n          <td>{{ company.company_name_in_arabic }} </td>\n        </tr>\n        \n        \n        {% if(supplier_address_doc) %}\n        <tr>\n          <td>{{ supplier_address_doc.address_line1}} </td>\n          <td>{{ supplier_address_doc.address_in_arabic}} </td>\n        </tr>\n        <tr>\n          <td>Phone: {{ supplier_address_doc.phone }}</td>\n          <td>\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}</td>\n        </tr>\n        <tr>\n          <td>Email: {{ supplier_address_doc.email_id }}</td>\n          <td>\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}</td>\n        </tr>\n        {% endif %}\n        \n        <!-- Customer Info -->\n        <tr>\n          <td>CUSTOMER:</td>\n          <td>\u0639\u0645\u064a\u0644:</td>\n        </tr>\n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n        <tr>\n          <td>Customer Tax Identification Number:</td>\n          <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:</td>\n        </tr>\n        <tr>\n          <td>{{ customer_tax_id }}</td>\n          <td>{{ customer_tax_id }}</td>\n        </tr>\n        {% endif %}\n        <tr>\n          <td> {{ doc.customer }}</td>\n          <td> {{ doc.customer_name_in_arabic }} </td>\n        </tr>\n        \n        {% if(customer_address) %}\n        <tr>\n          <td>{{ customer_address.address_line1}} </td>\n          <td>{{ customer_address.address_in_arabic}} </td>\n        </tr>\n        {% endif %}\n        \n        {% if(customer_shipping_address) %}\n        <tr>\n          <td>SHIPPING ADDRESS:</td>\n          <td>\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:</td>\n        </tr>\n        \n        <tr>\n          <td>{{ customer_shipping_address.address_line1}} </td>\n          <td>{{ customer_shipping_address.address_in_arabic}} </td>\n        </tr>\n        {% endif %}\n        \n\t\t{% if(doc.po_no) %}\n        <tr>\n          <td>OTHER INFORMATION</td>\n          <td>\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649</td>\n        </tr>\n        \n        <tr>\n          <td>Purchase Order Number: {{ doc.po_no }}</td>\n          <td>\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}</td>\n        </tr>\n        {% endif %}\n        \n        <tr>\n          <td>Payment Due Date: {{  doc.due_date}} </td>\n          <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{  doc.due_date}}</td>\n        </tr>\n      </tbody>\n    </table>\n\n    <!--Dynamic Colspan for total row columns-->\n    {% set col = namespace(one = 2, two = 1) %}\n    {% set length = doc.taxes | length %}\n    {% set length = length / 2 | round %}\n    {% set col.one = col.one + length %}\n    {% set col.two = col.two + length %}\n  \n    {%- if(doc.taxes | length % 2 > 0 ) -%}\n      {% set col.two = col.two + 1 %}\n    {% endif %}\n    \n    <!-- Items -->\n    {% set total = namespace(amount = 0) %}\n    <table class=\"ksa-invoice-table\">\n      <thead>\n        <tr>\n          <th>Nature of goods or services <br />\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a</th>\n          <th>\n            Unit price <br />\n            \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n          </th>\n          <th>\n            Quantity <br />\n            \u0627\u0644\u0643\u0645\u064a\u0629\n          </th>\n          <th>\n            Taxable Amount <br />\n            \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n          </th>\n          \n          {% for row in doc.taxes %}\n            <th style=\"min-width: 130px\">{{row.description}}</th>\n          {% endfor %}\n          \n          <th>\n            Total <br />\n            \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        {%- for item in doc.items -%}\n        {% set total.amount = item.amount %}\n        <tr>\n          <td>{{ item.item_code or item.item_name }}</td>\n          <td>{{ item.get_formatted(\"rate\") }}</td>\n          <td>{{ item.qty }}</td>\n          <td>{{ item.get_formatted(\"amount\") }}</td>\n           {% for row in doc.taxes %}\n                {% set data_object = json.loads(row.item_wise_tax_detail) %}\n                {% set key = item.item_code or item.item_name %}\n                {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n                <td>\n                   <div class=\"qr-flex\">\n                    {%- if(data_object[key][0])-%}\n                    <span>{{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}</span>\n                    {%- endif -%}\n                    <span>\n                    {%- if(data_object[key][1])-%}\n                        {{ frappe.format_value(tax_amount, currency=doc.currency) }}</span>\n                        {% set total.amount = total.amount + tax_amount %}\n                    {%- endif -%}\n                    </div>\n                </td>\n            {% endfor %}\n          <td>{{  frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}</td>\n        </tr>\n        {%- endfor -%}\n      </tbody>\n      <tfoot>\n        <tr>\n          <td>\n            {{ doc.get_formatted(\"total\") }} <br />\n            {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n          </td>\n          \n          <td colspan={{ col.one }} class=\"qr-rtl\">\n            \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n            <br />\n            \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n          </td>\n          <td colspan={{ col.two }}>\n            Total (Excluding VAT)\n            <br />\n            Total VAT\n          </td>\n          <td>\n            {{ doc.get_formatted(\"total\") }} <br />\n            {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n          </td>\n        </tr>\n        <tr>\n          <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n          <td  colspan={{  col.one }} class=\"qr-rtl\">\n              \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642</td>\n          <td  colspan={{  col.two }}>Total Amount Due</td>\n          <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n        </tr>\n      </tfoot>\n    </table>\n\n\t{%- if doc.terms -%}\n    <p>\n      {{doc.terms}}\n    </p>\n\t{%- endif -%}\n</div>\n",
+ "html": "<div class=\"ksa-vat-format\">\n    <div class=\"qr-flex\">\n        <div style=\"qr-flex: 1\">\n            <h2 class=\"invoice-heading\">TAX INVOICE</h2>\n            <h2 class=\"invoice-heading\">\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629</h2>\n        </div>\n        \n        <img class=\"qr-code\" src={{doc.ksa_einv_qr}}>\n    </div>\n    {% set company = frappe.get_doc(\"Company\", doc.company)%}\n    {% if (doc.company_address) %}\n        {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n    {% endif %}\n    \n    {% if(doc.customer_address) %}\n        {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n    {% endif %}\n    \n    {% if(doc.shipping_address_name) %}\n        {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n    {% endif %}  \n        \n    <table class=\"ksa-invoice-table two-columns\">\n      <thead>\n        <tr>\n          <th>{{ company.name }}</th>\n          <th style=\"text-align: right;\">{{ company.company_name_in_arabic }}</th>\n        </tr>\n      </thead>\n\n      <tbody>\n        <!-- Invoice Info -->\n        <tr>\n          <td>Invoice#: {{doc.name}}</td>\n          <td>\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}</td>\n        </tr>\n        <tr>\n          <td>Invoice Date: {{doc.posting_date}}</td>\n          <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}</td>\n        </tr>\n        <tr>\n          <td>Date of Supply:{{doc.posting_date}}</td>\n          <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}</td>\n        </tr>\n        \n        <!--Supplier Info -->\n        <tr>\n          <td>Supplier:</td>\n          <td>\u0627\u0644\u0645\u0648\u0631\u062f:</td>\n        </tr>\n\t\t{% if (company.tax_id) %}\n        <tr>\n          <td>Supplier Tax Identification Number:</td>\n          <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:</td>\n        </tr>\n        <tr>\n          <td>{{ company.tax_id }}</td>\n          <td>{{ company.tax_id }}</td>\n        </tr>\n        {% endif %}\n        <tr>\n          <td>{{ company.name }}</td>\n          <td>{{ company.company_name_in_arabic }} </td>\n        </tr>\n        \n        \n        {% if(supplier_address_doc) %}\n        <tr>\n          <td>{{ supplier_address_doc.address_line1}} </td>\n          <td>{{ supplier_address_doc.address_in_arabic}} </td>\n        </tr>\n        <tr>\n          <td>Phone: {{ supplier_address_doc.phone }}</td>\n          <td>\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}</td>\n        </tr>\n        <tr>\n          <td>Email: {{ supplier_address_doc.email_id }}</td>\n          <td>\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}</td>\n        </tr>\n        {% endif %}\n        \n        <!-- Customer Info -->\n        <tr>\n          <td>CUSTOMER:</td>\n          <td>\u0639\u0645\u064a\u0644:</td>\n        </tr>\n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n        <tr>\n          <td>Customer Tax Identification Number:</td>\n          <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:</td>\n        </tr>\n        <tr>\n          <td>{{ customer_tax_id }}</td>\n          <td>{{ customer_tax_id }}</td>\n        </tr>\n        {% endif %}\n        <tr>\n          <td> {{ doc.customer }}</td>\n          <td> {{ doc.customer_name_in_arabic }} </td>\n        </tr>\n        \n        {% if(customer_address) %}\n        <tr>\n          <td>{{ customer_address.address_line1}} </td>\n          <td>{{ customer_address.address_in_arabic}} </td>\n        </tr>\n        {% endif %}\n        \n        {% if(customer_shipping_address) %}\n        <tr>\n          <td>SHIPPING ADDRESS:</td>\n          <td>\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:</td>\n        </tr>\n        \n        <tr>\n          <td>{{ customer_shipping_address.address_line1}} </td>\n          <td>{{ customer_shipping_address.address_in_arabic}} </td>\n        </tr>\n        {% endif %}\n        \n\t\t{% if(doc.po_no) %}\n        <tr>\n          <td>OTHER INFORMATION</td>\n          <td>\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649</td>\n        </tr>\n        \n        <tr>\n          <td>Purchase Order Number: {{ doc.po_no }}</td>\n          <td>\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}</td>\n        </tr>\n        {% endif %}\n        \n        <tr>\n          <td>Payment Due Date: {{  doc.due_date}} </td>\n          <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{  doc.due_date}}</td>\n        </tr>\n      </tbody>\n    </table>\n\n    <!--Dynamic Colspan for total row columns-->\n    {% set col = namespace(one = 2, two = 1) %}\n    {% set length = doc.taxes | length %}\n    {% set length = length / 2 | round %}\n    {% set col.one = col.one + length %}\n    {% set col.two = col.two + length %}\n  \n    {%- if(doc.taxes | length % 2 > 0 ) -%}\n      {% set col.two = col.two + 1 %}\n    {% endif %}\n    \n    <!-- Items -->\n    {% set total = namespace(amount = 0) %}\n    <table class=\"ksa-invoice-table\">\n      <thead>\n        <tr>\n          <th>Nature of goods or services <br />\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a</th>\n          <th>\n            Unit price <br />\n            \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n          </th>\n          <th>\n            Quantity <br />\n            \u0627\u0644\u0643\u0645\u064a\u0629\n          </th>\n          <th>\n            Taxable Amount <br />\n            \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n          </th>\n          \n          {% for row in doc.taxes %}\n            <th style=\"min-width: 130px\">{{row.description}}</th>\n          {% endfor %}\n          \n          <th>\n            Total <br />\n            \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        {%- for item in doc.items -%}\n        {% set total.amount = item.amount %}\n        <tr>\n          <td>{{ item.item_code or item.item_name }}</td>\n          <td>{{ item.get_formatted(\"rate\") }}</td>\n          <td>{{ item.qty }}</td>\n          <td>{{ item.get_formatted(\"amount\") }}</td>\n           {% for row in doc.taxes %}\n                {% set data_object = json.loads(row.item_wise_tax_detail) %}\n                {% set key = item.item_code or item.item_name %}\n                {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n                <td>\n                   <div class=\"qr-flex\">\n                    {%- if(data_object[key][0])-%}\n                    <span>{{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}</span>\n                    {%- endif -%}\n                    <span>\n                    {%- if(data_object[key][1])-%}\n                        {{ frappe.format_value(tax_amount, currency=doc.currency) }}</span>\n                        {% set total.amount = total.amount + tax_amount %}\n                    {%- endif -%}\n                    </div>\n                </td>\n            {% endfor %}\n          <td>{{  frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}</td>\n        </tr>\n        {%- endfor -%}\n      </tbody>\n      <tfoot>\n        <tr>\n          <td>\n            {{ doc.get_formatted(\"total\") }} <br />\n            {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n          </td>\n          \n          <td colspan={{ col.one }} class=\"qr-rtl\">\n            \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n            <br />\n            \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n          </td>\n          <td colspan={{ col.two }}>\n            Total (Excluding VAT)\n            <br />\n            Total VAT\n          </td>\n          <td>\n            {{ doc.get_formatted(\"total\") }} <br />\n            {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n          </td>\n        </tr>\n        <tr>\n          <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n          <td  colspan={{  col.one }} class=\"qr-rtl\">\n              \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642</td>\n          <td  colspan={{  col.two }}>Total Amount Due</td>\n          <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n        </tr>\n      </tfoot>\n    </table>\n\n\t{%- if doc.terms -%}\n    <p>\n      {{doc.terms}}\n    </p>\n\t{%- endif -%}\n</div>\n",
  "idx": 0,
  "line_breaks": 0,
  "margin_bottom": 15.0,
  "margin_left": 15.0,
  "margin_right": 15.0,
  "margin_top": 15.0,
- "modified": "2021-11-29 13:47:37.870818",
+ "modified": "2021-12-07 13:43:38.018593",
  "modified_by": "Administrator",
  "module": "Regional",
  "name": "KSA VAT Invoice",
diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py
index e03ad37..1c1335e 100644
--- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py
+++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py
@@ -114,9 +114,11 @@
 
 	items = frappe.db.sql("""
 		select
-			`tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate,
-			`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty,
-			`tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_amount,
+			`tabSales Invoice Item`.gst_hsn_code,
+			`tabSales Invoice Item`.stock_uom,
+			sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
+			sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
+			sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
 			`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
 			`tabGST HSN Code`.description
 		from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
@@ -124,6 +126,8 @@
 			and `tabSales Invoice`.docstatus = 1
 			and `tabSales Invoice Item`.gst_hsn_code is not NULL
 			and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
+		group by
+			`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code
 
 		""" % (conditions, match_conditions), filters, as_dict=1)
 
diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py
new file mode 100644
index 0000000..86dc458
--- /dev/null
+++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+
+from unittest import TestCase
+
+import frappe
+
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
+	make_company as setup_company,
+)
+from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
+	make_customers as setup_customers,
+)
+from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
+	set_account_heads as setup_gst_settings,
+)
+from erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies import (
+	execute as run_report,
+)
+from erpnext.stock.doctype.item.test_item import make_item
+
+
+class TestHSNWiseSummaryReport(TestCase):
+	@classmethod
+	def setUpClass(cls):
+		setup_company()
+		setup_customers()
+		setup_gst_settings()
+		make_item("Golf Car", properties={ "gst_hsn_code": "999900" })
+
+	@classmethod
+	def tearDownClass(cls):
+		frappe.db.rollback()
+
+	def test_hsn_summary_for_invoice_with_duplicate_items(self):
+		si = create_sales_invoice(
+			company="_Test Company GST",
+			customer = "_Test GST Customer",
+			currency = "INR",
+			warehouse = "Finished Goods - _GST",
+			debit_to = "Debtors - _GST",
+			income_account = "Sales - _GST",
+			expense_account = "Cost of Goods Sold - _GST",
+			cost_center = "Main - _GST",
+			do_not_save=1
+		)
+
+		si.items = []
+		si.append("items", {
+			"item_code": "Golf Car",
+			"gst_hsn_code": "999900",
+			"qty": "1",
+			"rate": "120",
+			"cost_center": "Main - _GST"
+		})
+		si.append("items", {
+			"item_code": "Golf Car",
+			"gst_hsn_code": "999900",
+			"qty": "1",
+			"rate": "140",
+			"cost_center": "Main - _GST"
+		})
+		si.append("taxes", {
+			"charge_type": "On Net Total",
+			"account_head": "Output Tax IGST - _GST",
+			"cost_center": "Main - _GST",
+			"description": "IGST @ 18.0",
+			"rate": 18
+		})
+		si.posting_date = "2020-11-17"
+		si.submit()
+		si.reload()
+
+		[columns, data] = run_report(filters=frappe._dict({
+			"company": "_Test Company GST",
+			"gst_hsn_code": "999900",
+			"company_gstin": si.company_gstin,
+			"from_date": si.posting_date,
+			"to_date": si.posting_date
+		}))
+
+		filtered_rows = list(filter(lambda row: row['gst_hsn_code'] == "999900", data))
+		self.assertTrue(filtered_rows)
+
+		hsn_row = filtered_rows[0]
+		self.assertEquals(hsn_row['stock_qty'], 2.0)
+		self.assertEquals(hsn_row['total_amount'], 306.8)
diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py
index 38a089c..2e31c03 100644
--- a/erpnext/regional/saudi_arabia/setup.py
+++ b/erpnext/regional/saudi_arabia/setup.py
@@ -3,7 +3,7 @@
 
 import frappe
 from frappe.permissions import add_permission, update_permission_property
-from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields, add_print_formats
+from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields
 from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting
 from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
 
@@ -13,6 +13,16 @@
 	add_permissions()
 	make_custom_fields()
 
+def add_print_formats():
+	frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True)
+	frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True)
+	frappe.reload_doc("regional", "print_format", "tax_invoice", force=True)
+	frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
+	frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
+
+	for d in ('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice'):
+		frappe.db.set_value("Print Format", d, "disabled", 0)
+
 def add_permissions():
 	"""Add Permissions for KSA VAT Setting."""
 	add_permission('KSA VAT Setting', 'All', 0)
@@ -33,8 +43,16 @@
 	custom_fields = {
 		'Sales Invoice': [
 			dict(
-				fieldname='qr_code',
-				label='QR Code',
+				fieldname='ksa_einv_qr',
+				label='KSA E-Invoicing QR',
+				fieldtype='Attach Image',
+				read_only=1, no_copy=1, hidden=1
+			)
+		],
+		'POS Invoice': [
+			dict(
+				fieldname='ksa_einv_qr',
+				label='KSA E-Invoicing QR',
 				fieldtype='Attach Image',
 				read_only=1, no_copy=1, hidden=1
 			)
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
index 7d00d8b..a03c3f0 100644
--- a/erpnext/regional/saudi_arabia/utils.py
+++ b/erpnext/regional/saudi_arabia/utils.py
@@ -4,144 +4,146 @@
 
 import frappe
 from frappe import _
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
 from frappe.utils.data import add_to_date, get_time, getdate
 from pyqrcode import create as qr_create
 
 from erpnext import get_region
 
 
-def create_qr_code(doc, method):
-	"""Create QR Code after inserting Sales Inv
-	"""
-
+def create_qr_code(doc, method=None):
 	region = get_region(doc.company)
 	if region not in ['Saudi Arabia']:
 		return
 
-	# if QR Code field not present, do nothing
-	if not hasattr(doc, 'qr_code'):
-		return
+	# if QR Code field not present, create it. Invoices without QR are invalid as per law.
+	if not hasattr(doc, 'ksa_einv_qr'):
+		create_custom_fields({
+			doc.doctype: [
+				dict(
+					fieldname='ksa_einv_qr',
+					label='KSA E-Invoicing QR',
+					fieldtype='Attach Image',
+					read_only=1, no_copy=1, hidden=1
+				)
+			]
+		})
 
 	# Don't create QR Code if it already exists
-	qr_code = doc.get("qr_code")
+	qr_code = doc.get("ksa_einv_qr")
 	if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
 		return
 
-	meta = frappe.get_meta('Sales Invoice')
+	meta = frappe.get_meta(doc.doctype)
 
-	for field in meta.get_image_fields():
-		if field.fieldname == 'qr_code':
-			''' TLV conversion for
-			1. Seller's Name
-			2. VAT Number
-			3. Time Stamp
-			4. Invoice Amount
-			5. VAT Amount
-			'''
-			tlv_array = []
-			# Sellers Name
+	if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
+		''' TLV conversion for
+		1. Seller's Name
+		2. VAT Number
+		3. Time Stamp
+		4. Invoice Amount
+		5. VAT Amount
+		'''
+		tlv_array = []
+		# Sellers Name
 
-			seller_name = frappe.db.get_value(
-				'Company',
-				doc.company,
-				'company_name_in_arabic')
+		seller_name = frappe.db.get_value(
+			'Company',
+			doc.company,
+			'company_name_in_arabic')
 
-			if not seller_name:
-				frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
+		if not seller_name:
+			frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
 
-			tag = bytes([1]).hex()
-			length = bytes([len(seller_name.encode('utf-8'))]).hex()
-			value = seller_name.encode('utf-8').hex()
-			tlv_array.append(''.join([tag, length, value]))
+		tag = bytes([1]).hex()
+		length = bytes([len(seller_name.encode('utf-8'))]).hex()
+		value = seller_name.encode('utf-8').hex()
+		tlv_array.append(''.join([tag, length, value]))
 
-			# VAT Number
-			tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
-			if not tax_id:
-				frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
+		# VAT Number
+		tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
+		if not tax_id:
+			frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
 
-			tag = bytes([2]).hex()
-			length = bytes([len(tax_id)]).hex()
-			value = tax_id.encode('utf-8').hex()
-			tlv_array.append(''.join([tag, length, value]))
+		tag = bytes([2]).hex()
+		length = bytes([len(tax_id)]).hex()
+		value = tax_id.encode('utf-8').hex()
+		tlv_array.append(''.join([tag, length, value]))
 
-			# Time Stamp
-			posting_date = getdate(doc.posting_date)
-			time = get_time(doc.posting_time)
-			seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
-			time_stamp = add_to_date(posting_date, seconds=seconds)
-			time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
+		# Time Stamp
+		posting_date = getdate(doc.posting_date)
+		time = get_time(doc.posting_time)
+		seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
+		time_stamp = add_to_date(posting_date, seconds=seconds)
+		time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
 
-			tag = bytes([3]).hex()
-			length = bytes([len(time_stamp)]).hex()
-			value = time_stamp.encode('utf-8').hex()
-			tlv_array.append(''.join([tag, length, value]))
+		tag = bytes([3]).hex()
+		length = bytes([len(time_stamp)]).hex()
+		value = time_stamp.encode('utf-8').hex()
+		tlv_array.append(''.join([tag, length, value]))
 
-			# Invoice Amount
-			invoice_amount = str(doc.grand_total)
-			tag = bytes([4]).hex()
-			length = bytes([len(invoice_amount)]).hex()
-			value = invoice_amount.encode('utf-8').hex()
-			tlv_array.append(''.join([tag, length, value]))
+		# Invoice Amount
+		invoice_amount = str(doc.grand_total)
+		tag = bytes([4]).hex()
+		length = bytes([len(invoice_amount)]).hex()
+		value = invoice_amount.encode('utf-8').hex()
+		tlv_array.append(''.join([tag, length, value]))
 
-			# VAT Amount
-			vat_amount = str(doc.total_taxes_and_charges)
+		# VAT Amount
+		vat_amount = str(doc.total_taxes_and_charges)
 
-			tag = bytes([5]).hex()
-			length = bytes([len(vat_amount)]).hex()
-			value = vat_amount.encode('utf-8').hex()
-			tlv_array.append(''.join([tag, length, value]))
+		tag = bytes([5]).hex()
+		length = bytes([len(vat_amount)]).hex()
+		value = vat_amount.encode('utf-8').hex()
+		tlv_array.append(''.join([tag, length, value]))
 
-			# Joining bytes into one
-			tlv_buff = ''.join(tlv_array)
+		# Joining bytes into one
+		tlv_buff = ''.join(tlv_array)
 
-			# base64 conversion for QR Code
-			base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
+		# base64 conversion for QR Code
+		base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
 
-			qr_image = io.BytesIO()
-			url = qr_create(base64_string, error='L')
-			url.png(qr_image, scale=2, quiet_zone=1)
+		qr_image = io.BytesIO()
+		url = qr_create(base64_string, error='L')
+		url.png(qr_image, scale=2, quiet_zone=1)
 
-			name = frappe.generate_hash(doc.name, 5)
+		name = frappe.generate_hash(doc.name, 5)
 
-			# making file
-			filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
-			_file = frappe.get_doc({
-				"doctype": "File",
-				"file_name": filename,
-				"is_private": 0,
-				"content": qr_image.getvalue(),
-				"attached_to_doctype": doc.get("doctype"),
-				"attached_to_name": doc.get("name"),
-				"attached_to_field": "qr_code"
-			})
+		# making file
+		filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
+		_file = frappe.get_doc({
+			"doctype": "File",
+			"file_name": filename,
+			"is_private": 0,
+			"content": qr_image.getvalue(),
+			"attached_to_doctype": doc.get("doctype"),
+			"attached_to_name": doc.get("name"),
+			"attached_to_field": "ksa_einv_qr"
+		})
 
-			_file.save()
+		_file.save()
 
-			# assigning to document
-			doc.db_set('qr_code', _file.file_url)
-			doc.notify_update()
-
-			break
+		# assigning to document
+		doc.db_set('ksa_einv_qr', _file.file_url)
+		doc.notify_update()
 
 
-def delete_qr_code_file(doc, method):
-	"""Delete QR Code on deleted sales invoice"""
-
+def delete_qr_code_file(doc, method=None):
 	region = get_region(doc.company)
 	if region not in ['Saudi Arabia']:
 		return
 
-	if hasattr(doc, 'qr_code'):
-		if doc.get('qr_code'):
+	if hasattr(doc, 'ksa_einv_qr'):
+		if doc.get('ksa_einv_qr'):
 			file_doc = frappe.get_list('File', {
-				'file_url': doc.get('qr_code')
+				'file_url': doc.get('ksa_einv_qr')
 			})
 			if len(file_doc):
 				frappe.delete_doc('File', file_doc[0].name)
 
-def delete_vat_settings_for_company(doc, method):
+def delete_vat_settings_for_company(doc, method=None):
 	if doc.country != 'Saudi Arabia':
 		return
 
-	settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name})
-	settings_doc.delete()
\ No newline at end of file
+	if frappe.db.exists('KSA VAT Setting', doc.name):
+		frappe.delete_doc('KSA VAT Setting', doc.name)
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 7d6b74d..5301fd0 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -2,8 +2,6 @@
 # License: GNU General Public License v3. See license.txt
 
 
-import unittest
-
 import frappe
 from frappe.test_runner import make_test_records
 from frappe.utils import flt
@@ -11,7 +9,7 @@
 from erpnext.accounts.party import get_due_date
 from erpnext.exceptions import PartyDisabled, PartyFrozen
 from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding
-from erpnext.tests.utils import create_test_contact_and_address
+from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address
 
 test_ignore = ["Price List"]
 test_dependencies = ['Payment Term', 'Payment Terms Template']
@@ -19,7 +17,7 @@
 
 
 
-class TestCustomer(unittest.TestCase):
+class TestCustomer(ERPNextTestCase):
 	def setUp(self):
 		if not frappe.get_value('Item', '_Test Item'):
 			make_test_records('Item')
diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py
index 874a364..b951044 100644
--- a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py
+++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py
@@ -6,6 +6,7 @@
 import frappe
 
 from erpnext.controllers.queries import item_query
+from erpnext.tests.utils import ERPNextTestCase
 
 test_dependencies = ['Item', 'Customer', 'Supplier']
 
@@ -17,7 +18,7 @@
 	psi.based_on_value = args.get('based_on_value')
 	psi.insert()
 
-class TestPartySpecificItem(unittest.TestCase):
+class TestPartySpecificItem(ERPNextTestCase):
 	def setUp(self):
 		self.customer = frappe.get_last_doc("Customer")
 		self.supplier = frappe.get_last_doc("Supplier")
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index aa83726..4357201 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -1,15 +1,15 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
-import unittest
-
 import frappe
 from frappe.utils import add_days, add_months, flt, getdate, nowdate
 
+from erpnext.tests.utils import ERPNextTestCase
+
 test_dependencies = ["Product Bundle"]
 
 
-class TestQuotation(unittest.TestCase):
+class TestQuotation(ERPNextTestCase):
 	def test_make_quotation_without_terms(self):
 		quotation = make_quotation(do_not_save=1)
 		self.assertFalse(quotation.get('payment_schedule'))
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index e69e28d..cc95185 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -63,6 +63,8 @@
 		if not self.billing_status: self.billing_status = 'Not Billed'
 		if not self.delivery_status: self.delivery_status = 'Not Delivered'
 
+		self.reset_default_field_value("set_warehouse", "items", "warehouse")
+
 	def validate_po(self):
 		# validate p.o date v/s delivery date
 		if self.po_date and not self.skip_delivery_note:
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 2a0752e..42bc0b7 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -2,7 +2,6 @@
 # License: GNU General Public License v3. See license.txt
 
 import json
-import unittest
 
 import frappe
 import frappe.permissions
@@ -28,12 +27,14 @@
 )
 from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestSalesOrder(unittest.TestCase):
+class TestSalesOrder(ERPNextTestCase):
 
 	@classmethod
 	def setUpClass(cls):
+		super().setUpClass()
 		cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
 			"unlink_advance_payment_on_cancelation_of_order"))
 
@@ -42,6 +43,7 @@
 		# reset config to previous state
 		frappe.db.set_value("Accounts Settings", "Accounts Settings",
 			"unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
+		super().tearDownClass()
 
 	def tearDown(self):
 		frappe.set_user("Administrator")
diff --git a/erpnext/selling/form_tour/customer/customer.json b/erpnext/selling/form_tour/customer/customer.json
new file mode 100644
index 0000000..1de45b7
--- /dev/null
+++ b/erpnext/selling/form_tour/customer/customer.json
@@ -0,0 +1,29 @@
+{
+ "creation": "2021-11-23 10:44:13.185982",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-11-23 10:54:09.602358",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Customer",
+ "owner": "Administrator",
+ "reference_doctype": "Customer",
+ "save_on_complete": 1,
+ "steps": [
+  {
+   "description": "Enter the Full Name of the Customer",
+   "field": "",
+   "fieldname": "customer_name",
+   "fieldtype": "Data",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Full Name",
+   "parent_field": "",
+   "position": "Left",
+   "title": "Full Name"
+  }
+ ],
+ "title": "Customer"
+}
\ No newline at end of file
diff --git a/erpnext/selling/form_tour/quotation/quotation.json b/erpnext/selling/form_tour/quotation/quotation.json
new file mode 100644
index 0000000..2a2aa5e
--- /dev/null
+++ b/erpnext/selling/form_tour/quotation/quotation.json
@@ -0,0 +1,67 @@
+{
+ "creation": "2021-11-23 12:00:36.138824",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-11-23 12:02:48.010298",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Quotation",
+ "owner": "Administrator",
+ "reference_doctype": "Quotation",
+ "save_on_complete": 1,
+ "steps": [
+  {
+   "description": "Select a customer or lead for whom this quotation is being prepared. Let's select a Customer.",
+   "field": "",
+   "fieldname": "quotation_to",
+   "fieldtype": "Link",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Quotation To",
+   "parent_field": "",
+   "position": "Right",
+   "title": "Quotation To"
+  },
+  {
+   "description": "Select a specific Customer to whom this quotation will be sent.",
+   "field": "",
+   "fieldname": "party_name",
+   "fieldtype": "Dynamic Link",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Party",
+   "parent_field": "",
+   "position": "Right",
+   "title": "Party"
+  },
+  {
+   "child_doctype": "Quotation Item",
+   "description": "Select an item for which you will be quoting a price.",
+   "field": "",
+   "fieldname": "items",
+   "fieldtype": "Table",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Items",
+   "parent_field": "",
+   "parent_fieldname": "items",
+   "position": "Bottom",
+   "title": "Items"
+  },
+  {
+   "description": "You can select pre-populated Sales Taxes and Charges from here.",
+   "field": "",
+   "fieldname": "taxes",
+   "fieldtype": "Table",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Sales Taxes and Charges",
+   "parent_field": "",
+   "position": "Bottom",
+   "title": "Sales Taxes and Charges"
+  }
+ ],
+ "title": "Quotation"
+}
\ No newline at end of file
diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py
index 9c30afc..d62915f 100644
--- a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py
+++ b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py
@@ -2,8 +2,6 @@
 # For license information, please see license.txt
 
 
-import unittest
-
 from frappe.utils import add_months, nowdate
 
 from erpnext.selling.doctype.sales_order.sales_order import make_material_request
@@ -11,9 +9,10 @@
 from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import (
 	execute,
 )
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestPendingSOItemsForPurchaseRequest(unittest.TestCase):
+class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase):
     def test_result_for_partial_material_request(self):
         so = make_sales_order()
         mr=make_material_request(so.name)
diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py
index 8ffc5d6..f56cce2 100644
--- a/erpnext/selling/report/sales_analytics/test_analytics.py
+++ b/erpnext/selling/report/sales_analytics/test_analytics.py
@@ -2,15 +2,14 @@
 # For license information, please see license.txt
 
 
-import unittest
-
 import frappe
 
 from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
 from erpnext.selling.report.sales_analytics.sales_analytics import execute
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestAnalytics(unittest.TestCase):
+class TestAnalytics(ERPNextTestCase):
 	def test_sales_analytics(self):
 		frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
 
diff --git a/erpnext/setup/form_tour/company/company.json b/erpnext/setup/form_tour/company/company.json
new file mode 100644
index 0000000..c66abc0
--- /dev/null
+++ b/erpnext/setup/form_tour/company/company.json
@@ -0,0 +1,67 @@
+{
+ "creation": "2021-11-24 10:17:18.534917",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "first_document": 1,
+ "idx": 0,
+ "include_name_field": 0,
+ "is_standard": 1,
+ "modified": "2021-11-24 15:38:21.026582",
+ "modified_by": "Administrator",
+ "module": "Setup",
+ "name": "Company",
+ "owner": "Administrator",
+ "reference_doctype": "Company",
+ "save_on_complete": 0,
+ "steps": [
+  {
+   "description": "This is the default currency for this company.",
+   "field": "",
+   "fieldname": "default_currency",
+   "fieldtype": "Link",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Default Currency",
+   "parent_field": "",
+   "position": "Right",
+   "title": "Default Currency"
+  },
+  {
+   "description": "Here, you can add multiple addresses of the company",
+   "field": "",
+   "fieldname": "company_info",
+   "fieldtype": "Section Break",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Address & Contact",
+   "parent_field": "",
+   "position": "Top",
+   "title": "Address & Contact"
+  },
+  {
+   "description": "Here, you can set default Accounts, which will ease the creation of accounting entries.",
+   "field": "",
+   "fieldname": "default_settings",
+   "fieldtype": "Section Break",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Accounts Settings",
+   "parent_field": "",
+   "position": "Top",
+   "title": "Accounts Settings"
+  },
+  {
+   "description": "This setting is recommended if you wish to track the real-time stock balance in your books of account. This will allow the creation of a General Ledger entry for every stock transaction.",
+   "field": "",
+   "fieldname": "enable_perpetual_inventory",
+   "fieldtype": "Check",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Enable Perpetual Inventory",
+   "parent_field": "",
+   "position": "Right",
+   "title": "Enable Perpetual Inventory"
+  }
+ ],
+ "title": "Company"
+}
\ No newline at end of file
diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json
new file mode 100644
index 0000000..1b2dbc6
--- /dev/null
+++ b/erpnext/setup/module_onboarding/home/home.json
@@ -0,0 +1,62 @@
+{
+ "allow_roles": [
+  {
+   "role": "Accounts Manager"
+  },
+  {
+   "role": "Stock Manager"
+  },
+  {
+   "role": "Sales Manager"
+  },
+  {
+   "role": "Purchase Manager"
+  },
+  {
+   "role": "Manufacturing Manager"
+  },
+  {
+   "role": "Item Manager"
+  }
+ ],
+ "creation": "2021-11-22 12:19:15.888642",
+ "docstatus": 0,
+ "doctype": "Module Onboarding",
+ "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup",
+ "idx": 0,
+ "is_complete": 0,
+ "modified": "2021-12-15 14:23:52.460913",
+ "modified_by": "Administrator",
+ "module": "Setup",
+ "name": "Home",
+ "owner": "Administrator",
+ "steps": [
+  {
+   "step": "Company Set Up"
+  },
+  {
+   "step": "Navigation Help"
+  },
+  {
+   "step": "Data import"
+  },
+  {
+   "step": "Create an Item"
+  },
+  {
+   "step": "Create a Customer"
+  },
+  {
+   "step": "Create a Supplier"
+  },
+  {
+   "step": "Create a Quotation"
+  },
+  {
+   "step": "Letterhead"
+  }
+ ],
+ "subtitle": "Company, Item, Customer, Supplier, Navigation Help, Data Import, Letter Head, Quotation",
+ "success_message": "Masters are all set up!",
+ "title": "Let's Set Up Some Masters"
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json
new file mode 100644
index 0000000..6f65832
--- /dev/null
+++ b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Let's review your Company",
+ "creation": "2021-11-22 11:55:48.931427",
+ "description": "# Set Up a Company\n\nA company is a legal entity for which you will set up your books of account and create accounting transactions. In ERPNext, you can create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:22:18.317423",
+ "modified_by": "Administrator",
+ "name": "Company Set Up",
+ "owner": "Administrator",
+ "reference_document": "Company",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Set Up a Company",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json
new file mode 100644
index 0000000..f74d745
--- /dev/null
+++ b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Let\u2019s create your first Customer",
+ "creation": "2020-05-14 17:46:41.831517",
+ "description": "# Create a Customer\n\nThe Customer master is at the heart of your sales transactions. Customers are linked in Quotations, Sales Orders, Invoices, and Payments. Customers can be either numbered or identified by name (you would typically do this based on the number of customers you have).\n\nThrough Customer\u2019s master, you can effectively track essentials like:\n - Customer\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:20:31.197564",
+ "modified_by": "Administrator",
+ "name": "Create a Customer",
+ "owner": "Administrator",
+ "reference_document": "Customer",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Manage Customers",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json
new file mode 100644
index 0000000..8bdb621
--- /dev/null
+++ b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Let\u2019s create your first Quotation",
+ "creation": "2020-06-01 13:34:58.958641",
+ "description": "# Create a Quotation\n\nLet\u2019s get started with business transactions by creating your first Quotation. You can create a Quotation for an existing customer or a prospect. It will be an approved document, with items you sell and the proposed price + taxes applied. After completing the instructions, you will get a Quotation in a ready to share print format.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:21:31.675330",
+ "modified_by": "Administrator",
+ "name": "Create a Quotation",
+ "owner": "Administrator",
+ "reference_document": "Quotation",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Create your first Quotation",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json
new file mode 100644
index 0000000..9574141
--- /dev/null
+++ b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Let\u2019s create your first Supplier",
+ "creation": "2020-05-14 22:09:10.043554",
+ "description": "# Create a Supplier\n\nAlso known as Vendor, is a master at the center of your purchase transactions. Suppliers are linked in Request for Quotation, Purchase Orders, Receipts, and Payments. Suppliers can be either numbered or identified by name.\n\nThrough Supplier\u2019s master, you can effectively track essentials like:\n - Supplier\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:21:23.518301",
+ "modified_by": "Administrator",
+ "name": "Create a Supplier",
+ "owner": "Administrator",
+ "reference_document": "Supplier",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Manage Suppliers",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json
new file mode 100644
index 0000000..cd29683
--- /dev/null
+++ b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json
@@ -0,0 +1,23 @@
+{
+ "action": "Create Entry",
+ "action_label": "Create a new Item",
+ "creation": "2021-05-17 13:47:18.515052",
+ "description": "# Create an Item\n\nItem is a product, of a or service offered by your company, or something you buy as a part of your supplies or raw materials.\n\nItems are integral to everything you do in ERPNext - from billing, purchasing to managing inventory. Everything you buy or sell, whether it is a physical product or a service is an Item. Items can be stock, non-stock, variants, serialized, batched, assets etc.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "form_tour": "Item General",
+ "idx": 0,
+ "intro_video_url": "",
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:19:56.297772",
+ "modified_by": "Administrator",
+ "name": "Create an Item",
+ "owner": "Administrator",
+ "reference_document": "Item",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Manage Items",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/data_import/data_import.json b/erpnext/setup/onboarding_step/data_import/data_import.json
new file mode 100644
index 0000000..48741dc
--- /dev/null
+++ b/erpnext/setup/onboarding_step/data_import/data_import.json
@@ -0,0 +1,21 @@
+{
+ "action": "Watch Video",
+ "action_label": "Learn more about data migration",
+ "creation": "2021-05-19 05:29:16.809610",
+ "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc). If you are migrating from [Tally](https://tallysolutions.com/) or [Quickbooks](https://quickbooks.intuit.com/in/), we got special migration tools for you.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 13:10:57.346422",
+ "modified_by": "Administrator",
+ "name": "Data import",
+ "owner": "Administrator",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Import Data from Spreadsheet",
+ "validate_action": 1,
+ "video_url": "https://youtu.be/DQyqeurPI64"
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/letterhead/letterhead.json b/erpnext/setup/onboarding_step/letterhead/letterhead.json
new file mode 100644
index 0000000..8e1bb8c
--- /dev/null
+++ b/erpnext/setup/onboarding_step/letterhead/letterhead.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Let\u2019s setup your first Letter Head",
+ "creation": "2021-11-22 12:36:34.583783",
+ "description": "# Create a Letter Head\n\nA Letter Head contains your organization's name, logo, address, etc which appears at the header and footer portion in documents. You can learn more about Setting up Letter Head in ERPNext here.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:21:39.037742",
+ "modified_by": "Administrator",
+ "name": "Letterhead",
+ "owner": "Administrator",
+ "reference_document": "Letter Head",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Setup Your Letterhead",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json
new file mode 100644
index 0000000..388853d
--- /dev/null
+++ b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json
@@ -0,0 +1,21 @@
+{
+ "action": "Watch Video",
+ "action_label": "Learn about  Navigation options",
+ "creation": "2021-11-22 12:09:52.233872",
+ "description": "# Navigation in ERPNext\n\nEase of navigating and browsing around the ERPNext is one of our core strengths. In the following video, you will learn how to reach a specific feature in ERPNext via module page or awesome bar\u2019s shortcut.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-12-15 14:20:55.441678",
+ "modified_by": "Administrator",
+ "name": "Navigation Help",
+ "owner": "Administrator",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "How to Navigate in ERPNext",
+ "validate_action": 1,
+ "video_url": "https://youtu.be/j60xyNFqX_A"
+}
\ No newline at end of file
diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py
index e4b1fa2..ca1f57e 100644
--- a/erpnext/setup/setup_wizard/operations/defaults_setup.py
+++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py
@@ -68,6 +68,8 @@
 
 	hr_settings.send_interview_feedback_reminder = 1
 	hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
+
+	hr_settings.exit_questionnaire_notification_template = _("Exit Questionnaire Notification")
 	hr_settings.save()
 
 def set_no_copy_fields_in_variant_settings():
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 98f9119..97d850b 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -278,6 +278,11 @@
 	records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response,
 		'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}]
 
+	response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html'))
+
+	records += [{'doctype': 'Email Template', 'name': _('Exit Questionnaire Notification'), 'response': response,
+		'subject': _('Exit Questionnaire Notification'), 'owner': frappe.session.user}]
+
 	base_path = frappe.get_app_path("erpnext", "stock", "doctype")
 	response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html"))
 
diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json
index 4e1ccf9..f9c585c0 100644
--- a/erpnext/setup/workspace/home/home.json
+++ b/erpnext/setup/workspace/home/home.json
@@ -1,13 +1,18 @@
 {
  "charts": [],
- "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
+ "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports &amp; Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
  "creation": "2020-01-23 13:46:38.833076",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
  "docstatus": 0,
  "doctype": "Workspace",
+ "extends_another_page": 0,
  "for_user": "",
  "hide_custom": 0,
  "icon": "getting-started",
  "idx": 0,
+ "is_default": 0,
+ "is_standard": 0,
  "label": "Home",
  "links": [
   {
@@ -271,12 +276,14 @@
    "type": "Link"
   }
  ],
- "modified": "2021-08-10 15:33:20.704741",
+ "modified": "2021-11-22 12:50:15.771366",
  "modified_by": "Administrator",
  "module": "Setup",
  "name": "Home",
  "owner": "Administrator",
  "parent_page": "",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
  "public": 1,
  "restrict_to_domain": "",
  "roles": [],
@@ -309,4 +316,4 @@
   }
  ],
  "title": "Home"
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index a33134b..37b5411 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -43,9 +43,9 @@
 				frappe.qb
 					.from_(wo)
 					.from_(wo_item)
-					.select(Case()
-							.when(wo.skip_transfer == 0, Sum(wo_item.required_qty - wo_item.transferred_qty))
-							.else_(Sum(wo_item.required_qty - wo_item.consumed_qty))
+					.select(Sum(Case()
+							.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
+							.else_(wo_item.required_qty - wo_item.consumed_qty))
 						)
 					.where(
 						(wo_item.item_code == self.item_code)
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 5268460..70d48a4 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -138,6 +138,7 @@
 		self.update_current_stock()
 
 		if not self.installation_status: self.installation_status = 'Not Installed'
+		self.reset_default_field_value("set_warehouse", "items", "warehouse")
 
 	def validate_with_previous_doc(self):
 		super(DeliveryNote, self).validate_with_previous_doc({
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 4f4e691..29abd45 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -361,8 +361,7 @@
    "fieldname": "valuation_method",
    "fieldtype": "Select",
    "label": "Valuation Method",
-   "options": "\nFIFO\nMoving Average",
-   "set_only_once": 1
+   "options": "\nFIFO\nMoving Average"
   },
   {
    "depends_on": "is_stock_item",
@@ -1035,7 +1034,7 @@
  "image_field": "image",
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2021-12-03 08:32:03.869294",
+ "modified": "2021-12-14 04:13:16.857534",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Item",
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index d717c50..103e8d6 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -80,6 +80,9 @@
 		# NOTE: Since Item BOM and FG quantities are combined, using current data, it cannot be validated
 		# Though the creation of Material Request from a Production Plan can be rethought to fix this
 
+		self.reset_default_field_value("set_warehouse", "items", "warehouse")
+		self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+
 	def set_title(self):
 		'''Set title as comma separated list of items'''
 		if not self.title:
diff --git a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
index 29c4193..4270839 100644
--- a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
+++ b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
@@ -1,451 +1,140 @@
 {
- "allow_copy": 0, 
- "allow_import": 0, 
- "allow_rename": 0, 
- "autoname": "hash", 
- "beta": 0, 
- "creation": "2013-04-08 13:10:16", 
- "custom": 0, 
- "docstatus": 0, 
- "doctype": "DocType", 
- "document_type": "Document", 
- "editable_grid": 1, 
- "engine": "InnoDB", 
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2013-04-08 13:10:16",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "item_code",
+  "column_break_2",
+  "item_name",
+  "batch_no",
+  "desc_section",
+  "description",
+  "quantity_section",
+  "qty",
+  "net_weight",
+  "column_break_10",
+  "stock_uom",
+  "weight_uom",
+  "page_break",
+  "dn_detail"
+ ],
  "fields": [
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "item_code", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 1, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Item Code", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "Item", 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "print_width": "100px", 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 1, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0, 
+   "fieldname": "item_code",
+   "fieldtype": "Link",
+   "in_global_search": 1,
+   "in_list_view": 1,
+   "label": "Item Code",
+   "options": "Item",
+   "print_width": "100px",
+   "reqd": 1,
    "width": "100px"
-  }, 
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "column_break_2", 
-   "fieldtype": "Column Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "column_break_2",
+   "fieldtype": "Column Break"
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "item_name", 
-   "fieldtype": "Data", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Item Name", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "item_code.item_name",
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "print_width": "200px", 
-   "read_only": 1, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0, 
+   "fetch_from": "item_code.item_name",
+   "fieldname": "item_name",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Item Name",
+   "print_width": "200px",
+   "read_only": 1,
    "width": "200px"
-  }, 
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "batch_no", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Batch No", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "Batch", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "batch_no",
+   "fieldtype": "Link",
+   "label": "Batch No",
+   "options": "Batch"
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 1, 
-   "columns": 0, 
-   "fieldname": "desc_section", 
-   "fieldtype": "Section Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Description", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "collapsible": 1,
+   "fieldname": "desc_section",
+   "fieldtype": "Section Break",
+   "label": "Description"
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "description", 
-   "fieldtype": "Text Editor", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Description", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "description",
+   "fieldtype": "Text Editor",
+   "label": "Description"
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "quantity_section", 
-   "fieldtype": "Section Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Quantity", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "quantity_section",
+   "fieldtype": "Section Break",
+   "label": "Quantity"
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "qty", 
-   "fieldtype": "Float", 
-   "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": "Quantity", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "print_width": "100px", 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 1, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0, 
+   "fieldname": "qty",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Quantity",
+   "print_width": "100px",
+   "reqd": 1,
    "width": "100px"
-  }, 
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "net_weight", 
-   "fieldtype": "Float", 
-   "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": "Net Weight", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "print_width": "100px", 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0, 
+   "fieldname": "net_weight",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Net Weight",
+   "print_width": "100px",
    "width": "100px"
-  }, 
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "column_break_10", 
-   "fieldtype": "Column Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "column_break_10",
+   "fieldtype": "Column Break"
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "stock_uom", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "UOM", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "UOM", 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "print_width": "100px", 
-   "read_only": 1, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0, 
+   "fieldname": "stock_uom",
+   "fieldtype": "Link",
+   "label": "UOM",
+   "options": "UOM",
+   "print_width": "100px",
+   "read_only": 1,
    "width": "100px"
-  }, 
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "weight_uom", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Weight UOM", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "UOM", 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "print_width": "100px", 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0, 
+   "fieldname": "weight_uom",
+   "fieldtype": "Link",
+   "label": "Weight UOM",
+   "options": "UOM",
+   "print_width": "100px",
    "width": "100px"
-  }, 
+  },
   {
-   "allow_on_submit": 1, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "page_break", 
-   "fieldtype": "Check", 
-   "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": "Page Break", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "allow_on_submit": 1,
+   "default": "0",
+   "fieldname": "page_break",
+   "fieldtype": "Check",
+   "in_list_view": 1,
+   "label": "Page Break"
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "dn_detail", 
-   "fieldtype": "Data", 
-   "hidden": 1, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "DN Detail", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
+   "fieldname": "dn_detail",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "in_list_view": 1,
+   "label": "DN Detail"
   }
- ], 
- "hide_heading": 0, 
- "hide_toolbar": 0, 
- "idx": 1, 
- "image_view": 0, 
- "in_create": 0, 
-
- "is_submittable": 0, 
- "issingle": 0, 
- "istable": 1, 
- "max_attachments": 0, 
- "modified": "2018-06-01 07:21:58.220980",
- "modified_by": "Administrator", 
- "module": "Stock", 
- "name": "Packing Slip Item", 
- "owner": "Administrator", 
- "permissions": [], 
- "quick_entry": 0, 
- "read_only": 0, 
- "read_only_onload": 0, 
- "show_name_in_global_search": 0, 
- "track_changes": 1, 
- "track_seen": 0
+ ],
+ "idx": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-12-14 01:22:00.715935",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Packing Slip Item",
+ "naming_rule": "Random",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 762f45f..c97b306 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -118,6 +118,10 @@
 		if getdate(self.posting_date) > getdate(nowdate()):
 			throw(_("Posting Date cannot be future date"))
 
+		self.reset_default_field_value("set_warehouse", "items", "warehouse")
+		self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
+		self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+
 
 	def validate_cwip_accounts(self):
 		for item in self.get('items'):
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
index d08dc3e..eea2879 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
@@ -59,7 +59,7 @@
 	},
 
 	item_code: function(frm) {
-		if (frm.doc.item_code) {
+		if (frm.doc.item_code && !frm.doc.quality_inspection_template) {
 			return frm.call({
 				method: "get_quality_inspection_template",
 				doc: frm.doc,
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 913ee15..4e3b80a 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -18,6 +18,15 @@
 		if not self.readings and self.item_code:
 			self.get_item_specification_details()
 
+		if self.inspection_type=="In Process" and self.reference_type=="Job Card":
+			item_qi_template = frappe.db.get_value("Item", self.item_code, 'quality_inspection_template')
+			parameters = get_template_details(item_qi_template)
+			for reading in self.readings:
+				for d in parameters:
+					if reading.specification == d.specification:
+						reading.update(d)
+						reading.status = "Accepted"
+
 		if self.readings:
 			self.inspect_and_set_status()
 
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 01cceb1..b2ad07f 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -168,8 +168,8 @@
 	for row in riv_entries:
 		doc = frappe.get_doc('Repost Item Valuation', row.name)
 		if doc.status in ('Queued', 'In Progress'):
-			doc.deduplicate_similar_repost()
 			repost(doc)
+			doc.deduplicate_similar_repost()
 
 	riv_entries = get_repost_item_valuation_entries()
 	if riv_entries:
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 de79316..78b432d 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
@@ -4,12 +4,14 @@
 import unittest
 
 import frappe
+from frappe.utils import nowdate
 
 from erpnext.controllers.stock_controller import create_item_wise_repost_entries
 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.utils import PendingRepostingError
 
 
 class TestRepostItemValuation(unittest.TestCase):
@@ -138,3 +140,25 @@
 		# to avoid breaking other tests accidentaly
 		riv4.set_status("Skipped")
 		riv3.set_status("Skipped")
+
+	def test_stock_freeze_validation(self):
+
+		today = nowdate()
+
+		riv = frappe.get_doc(
+			doctype="Repost Item Valuation",
+			item_code="_Test Item",
+			warehouse="_Test Warehouse - _TC",
+			based_on="Item and Warehouse",
+			posting_date=today,
+			posting_time="00:01:00",
+		)
+		riv.flags.dont_run_in_test = True # keep it queued
+		riv.submit()
+
+		stock_settings = frappe.get_doc("Stock Settings")
+		stock_settings.stock_frozen_upto = today
+
+		self.assertRaises(PendingRepostingError, stock_settings.save)
+
+		riv.set_status("Skipped")
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index a38dfa5..a00d63e 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -103,6 +103,8 @@
 		self.set_actual_qty()
 		self.calculate_rate_and_amount()
 		self.validate_putaway_capacity()
+		self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
+		self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
 
 	def on_submit(self):
 		self.update_stock_ledger()
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 5ef0770..5a9e77e 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -24,7 +24,8 @@
 from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
 	create_stock_reconciliation,
 )
-from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle
+from erpnext.tests.utils import ERPNextTestCase, change_settings
 
 
 def get_sle(**args):
@@ -38,7 +39,7 @@
 		order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition,
 		values, as_dict=1)
 
-class TestStockEntry(unittest.TestCase):
+class TestStockEntry(ERPNextTestCase):
 	def tearDown(self):
 		frappe.set_user("Administrator")
 		frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
@@ -928,6 +929,83 @@
 		distributed_costs = [d.additional_cost for d in se.items]
 		self.assertEqual([40.0, 60.0], distributed_costs)
 
+	@change_settings("Stock Settings", {"allow_negative_stock": 0})
+	def test_future_negative_sle(self):
+		# Initialize item, batch, warehouse, opening qty
+		item_code = '_Test Future Neg Item'
+		batch_no = '_Test Future Neg Batch'
+		warehouses = [
+			'_Test Future Neg Warehouse Source',
+			'_Test Future Neg Warehouse Destination'
+		]
+		warehouse_names = initialize_records_for_future_negative_sle_test(
+			item_code, batch_no, warehouses,
+			opening_qty=2, posting_date='2021-07-01'
+		)
+
+		# Executing an illegal sequence should raise an error
+		sequence_of_entries = [
+			dict(item_code=item_code,
+				qty=2,
+				from_warehouse=warehouse_names[0],
+				to_warehouse=warehouse_names[1],
+				batch_no=batch_no,
+				posting_date='2021-07-03',
+				purpose='Material Transfer'),
+			dict(item_code=item_code,
+				qty=2,
+				from_warehouse=warehouse_names[1],
+				to_warehouse=warehouse_names[0],
+				batch_no=batch_no,
+				posting_date='2021-07-04',
+				purpose='Material Transfer'),
+			dict(item_code=item_code,
+				qty=2,
+				from_warehouse=warehouse_names[0],
+				to_warehouse=warehouse_names[1],
+				batch_no=batch_no,
+				posting_date='2021-07-02',          # Illegal SE
+				purpose='Material Transfer')
+		]
+
+		self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries)
+
+	@change_settings("Stock Settings", {"allow_negative_stock": 0})
+	def test_future_negative_sle_batch(self):
+		from erpnext.stock.doctype.batch.test_batch import TestBatch
+
+		# Initialize item, batch, warehouse, opening qty
+		item_code = '_Test MultiBatch Item'
+		TestBatch.make_batch_item(item_code)
+
+		batch_nos = [] # store generate batches
+		warehouse = '_Test Warehouse - _TC'
+
+		se1 = make_stock_entry(
+				item_code=item_code,
+				qty=2,
+				to_warehouse=warehouse,
+				posting_date='2021-09-01',
+				purpose='Material Receipt'
+			)
+		batch_nos.append(se1.items[0].batch_no)
+		se2 = make_stock_entry(
+				item_code=item_code,
+				qty=2,
+				to_warehouse=warehouse,
+				posting_date='2021-09-03',
+				purpose='Material Receipt'
+			)
+		batch_nos.append(se2.items[0].batch_no)
+
+		with self.assertRaises(NegativeStockError) as nse:
+			make_stock_entry(item_code=item_code,
+				qty=1,
+				from_warehouse=warehouse,
+				batch_no=batch_nos[1],
+				posting_date='2021-09-02', # backdated consumption of 2nd batch
+				purpose='Material Issue')
+
 def make_serialized_item(**args):
 	args = frappe._dict(args)
 	se = frappe.copy_doc(test_records[0])
@@ -998,3 +1076,31 @@
 		]
 
 test_records = frappe.get_test_records('Stock Entry')
+
+def initialize_records_for_future_negative_sle_test(
+		item_code, batch_no, warehouses, opening_qty, posting_date):
+	from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch
+	from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+		create_stock_reconciliation,
+	)
+	from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+	TestBatch.make_batch_item(item_code)
+	make_new_batch(item_code=item_code, batch_id=batch_no)
+	warehouse_names = [create_warehouse(w) for w in warehouses]
+	create_stock_reconciliation(
+		purpose='Opening Stock',
+		posting_date=posting_date,
+		posting_time='20:00:20',
+		item_code=item_code,
+		warehouse=warehouse_names[0],
+		valuation_rate=100,
+		qty=opening_qty,
+		batch_no=batch_no,
+	)
+	return warehouse_names
+
+
+def create_stock_entries(sequence_of_entries):
+	for entry_detail in sequence_of_entries:
+		make_stock_entry(**entry_detail)
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 93bca7a..c538307 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -8,7 +8,7 @@
 from frappe import _
 from frappe.core.doctype.role.role import get_users
 from frappe.model.document import Document
-from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate
+from frappe.utils import add_days, cint, formatdate, get_datetime, getdate
 
 from erpnext.accounts.utils import get_fiscal_year
 from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
@@ -43,7 +43,6 @@
 
 	def on_submit(self):
 		self.check_stock_frozen_date()
-		self.actual_amt_check()
 		self.calculate_batch_qty()
 
 		if not self.get("via_landed_cost_voucher"):
@@ -57,18 +56,6 @@
 				"sum(actual_qty)") or 0
 			frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
 
-	def actual_amt_check(self):
-		"""Validate that qty at warehouse for selected batch is >=0"""
-		if self.batch_no and not self.get("allow_negative_stock"):
-			batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty)
-				from `tabStock Ledger Entry`
-				where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""",
-				(self.warehouse, self.item_code, self.batch_no))[0][0])
-
-			if batch_bal_after_transaction < 0:
-				frappe.throw(_("Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3}")
-					.format(self.batch_no, batch_bal_after_transaction, self.item_code, self.warehouse))
-
 	def validate_mandatory(self):
 		mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
 		for k in mandatory:
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 1de48b6..c1293cb 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -11,6 +11,8 @@
 from frappe.utils import cint
 from frappe.utils.html_utils import clean_html
 
+from erpnext.stock.utils import check_pending_reposting
+
 
 class StockSettings(Document):
 	def validate(self):
@@ -36,6 +38,7 @@
 		self.validate_warehouses()
 		self.cant_change_valuation_method()
 		self.validate_clean_description_html()
+		self.validate_pending_reposts()
 
 	def validate_warehouses(self):
 		warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
@@ -64,6 +67,11 @@
 			# changed to text
 			frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test)
 
+	def validate_pending_reposts(self):
+		if self.stock_frozen_upto:
+			check_pending_reposting(self.stock_frozen_upto)
+
+
 	def on_update(self):
 		self.toggle_warehouse_field_for_inter_warehouse_transfer()
 
diff --git a/erpnext/stock/form_tour/item/item.json b/erpnext/stock/form_tour/item/item.json
index 821e91b..5369366 100644
--- a/erpnext/stock/form_tour/item/item.json
+++ b/erpnext/stock/form_tour/item/item.json
@@ -2,15 +2,17 @@
  "creation": "2021-08-24 17:56:40.754909",
  "docstatus": 0,
  "doctype": "Form Tour",
+ "first_document": 0,
  "idx": 0,
+ "include_name_field": 0,
  "is_standard": 1,
- "modified": "2021-08-24 18:04:50.928431",
+ "modified": "2021-11-24 17:59:44.559001",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Item",
  "owner": "Administrator",
  "reference_doctype": "Item",
- "save_on_complete": 0,
+ "save_on_complete": 1,
  "steps": [
   {
    "description": "Enter code for Asset Item",
@@ -37,13 +39,26 @@
    "title": "Asset Item Name"
   },
   {
+   "description": "Select an Item Group",
+   "field": "",
+   "fieldname": "item_group",
+   "fieldtype": "Link",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Item Group",
+   "parent_field": "",
+   "position": "Right",
+   "title": "Item Group"
+  },
+  {
    "description": "Check this field to make this an Asset Item",
    "field": "",
    "fieldname": "is_fixed_asset",
    "fieldtype": "Check",
-   "has_next_condition": 0,
+   "has_next_condition": 1,
    "is_table_field": 0,
    "label": "Is Fixed Asset",
+   "next_step_condition": "eval:doc.is_fixed_asset",
    "parent_field": "",
    "position": "Bottom",
    "title": "Is this a Fixed Asset?"
@@ -53,9 +68,10 @@
    "field": "",
    "fieldname": "auto_create_assets",
    "fieldtype": "Check",
-   "has_next_condition": 0,
+   "has_next_condition": 1,
    "is_table_field": 0,
    "label": "Auto Create Assets on Purchase",
+   "next_step_condition": "eval:doc.auto_create_assets",
    "parent_field": "",
    "position": "Bottom",
    "title": "Auto Create Asset on Purchase"
@@ -69,7 +85,7 @@
    "is_table_field": 0,
    "label": "Asset Category",
    "parent_field": "",
-   "position": "Bottom",
+   "position": "Left",
    "title": "Asset Category"
   },
   {
@@ -81,9 +97,9 @@
    "is_table_field": 0,
    "label": "Asset Naming Series",
    "parent_field": "",
-   "position": "Bottom",
+   "position": "Left",
    "title": "Asset Naming Series"
   }
  ],
  "title": "Item"
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/form_tour/item_general/item_general.json b/erpnext/stock/form_tour/item_general/item_general.json
new file mode 100644
index 0000000..b468d27
--- /dev/null
+++ b/erpnext/stock/form_tour/item_general/item_general.json
@@ -0,0 +1,79 @@
+{
+ "creation": "2021-12-02 10:37:55.433087",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "first_document": 0,
+ "idx": 0,
+ "include_name_field": 0,
+ "is_standard": 1,
+ "modified": "2021-12-02 10:37:55.433087",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Item General",
+ "owner": "Administrator",
+ "reference_doctype": "Item",
+ "save_on_complete": 1,
+ "steps": [
+  {
+   "description": "Enter code for the Item",
+   "field": "",
+   "fieldname": "item_code",
+   "fieldtype": "Data",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Item Code",
+   "parent_field": "",
+   "position": "Right",
+   "title": "Item Code"
+  },
+  {
+   "description": "Enter name for the Item",
+   "field": "",
+   "fieldname": "item_name",
+   "fieldtype": "Data",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Item Name",
+   "parent_field": "",
+   "position": "Right",
+   "title": "Item Name"
+  },
+  {
+   "description": "Select an Item Group",
+   "field": "",
+   "fieldname": "item_group",
+   "fieldtype": "Link",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Item Group",
+   "parent_field": "",
+   "position": "Right",
+   "title": "Item Group"
+  },
+  {
+   "description": "This is the default measuring unit that you will use for your product. It could be Nos, Kgs, Meters, etc.",
+   "field": "",
+   "fieldname": "stock_uom",
+   "fieldtype": "Link",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Default Unit of Measure",
+   "parent_field": "",
+   "position": "Right",
+   "title": "Default Unit of Measurement"
+  },
+  {
+   "description": "When creating an Item, entering a value for this field will automatically create an Item Price at the backend. Entering a value after the Item has been saved will not work. In this case, the Item Price is created from any transactions with the Item.",
+   "field": "",
+   "fieldname": "standard_rate",
+   "fieldtype": "Currency",
+   "has_next_condition": 0,
+   "is_table_field": 0,
+   "label": "Standard Selling Rate",
+   "parent_field": "",
+   "position": "Left",
+   "title": "Standard Selling Rate"
+  }
+ ],
+ "title": "Item General"
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
index 314f160..3f49065 100644
--- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
+++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
@@ -48,6 +48,7 @@
 	conditions = [get_item_group_condition(filters.get("item_group"))]
 	if filters.get("brand"):
 		conditions.append("item.brand=%(brand)s")
+	conditions.append("is_stock_item = 1")
 
 	return frappe.db.sql("""select name, item_name, description, brand, item_group,
 		safety_stock, lead_time_days from `tabItem` item where {}"""
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index c0b89fd..3c7b26b 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -167,7 +167,7 @@
 			sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference,
 			sle.item_code as name, sle.voucher_no, sle.stock_value, sle.batch_no
 		from
-			`tabStock Ledger Entry` sle force index (posting_sort_index)
+			`tabStock Ledger Entry` sle
 		where sle.docstatus < 2 %s %s
 		and is_cancelled = 0
 		order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" % #nosec
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/__init__.py b/erpnext/stock/report/stock_ledger_invariant_check/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_invariant_check/__init__.py
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js
new file mode 100644
index 0000000..c484516
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js
@@ -0,0 +1,43 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+const DIFFERNCE_FIELD_NAMES = [
+	"difference_in_qty",
+	"fifo_qty_diff",
+	"fifo_value_diff",
+	"fifo_valuation_diff",
+	"valuation_diff",
+	"fifo_difference_diff"
+];
+
+frappe.query_reports["Stock Ledger Invariant Check"] = {
+	"filters": [
+		{
+			"fieldname": "item_code",
+			"fieldtype": "Link",
+			"label": "Item",
+			"mandatory": 1,
+			"options": "Item",
+			get_query: function() {
+				return {
+					filters: {is_stock_item: 1, has_serial_no: 0}
+				}
+			}
+		},
+		{
+			"fieldname": "warehouse",
+			"fieldtype": "Link",
+			"label": "Warehouse",
+			"mandatory": 1,
+			"options": "Warehouse",
+		}
+	],
+	formatter (value, row, column, data, default_formatter) {
+		value = default_formatter(value, row, column, data);
+		if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
+			value = "<span style='color:red'>" + value + "</span>";
+		}
+		return value;
+	},
+};
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.json b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.json
new file mode 100644
index 0000000..d28fe0f
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.json
@@ -0,0 +1,26 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-12-16 06:31:23.290916",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-12-16 09:55:58.341764",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Stock Ledger Invariant Check",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Stock Ledger Invariant Check",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "System Manager"
+  }
+ ]
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..ca47a1e
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -0,0 +1,236 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# License: GNU GPL v3. See LICENSE
+
+import json
+
+import frappe
+
+SLE_FIELDS = (
+	"name",
+	"posting_date",
+	"posting_time",
+	"creation",
+	"voucher_type",
+	"voucher_no",
+	"actual_qty",
+	"qty_after_transaction",
+	"incoming_rate",
+	"outgoing_rate",
+	"stock_queue",
+	"batch_no",
+	"stock_value",
+	"stock_value_difference",
+	"valuation_rate",
+)
+
+
+def execute(filters=None):
+	columns = get_columns()
+	data = get_data(filters)
+	return columns, data
+
+
+def get_data(filters):
+	sles = get_stock_ledger_entries(filters)
+	return add_invariant_check_fields(sles)
+
+
+def get_stock_ledger_entries(filters):
+	return frappe.get_all(
+		"Stock Ledger Entry",
+		fields=SLE_FIELDS,
+		filters={
+			"item_code": filters.item_code,
+			"warehouse": filters.warehouse,
+			"is_cancelled": 0
+		},
+		order_by="timestamp(posting_date, posting_time), creation",
+	)
+
+
+def add_invariant_check_fields(sles):
+	balance_qty = 0.0
+	for idx, sle in enumerate(sles):
+		queue = json.loads(sle.stock_queue)
+
+		fifo_qty = 0.0
+		fifo_value = 0.0
+		for qty, rate in queue:
+			fifo_qty += qty
+			fifo_value += qty * rate
+
+		balance_qty += sle.actual_qty
+		if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
+			balance_qty = sle.qty_after_transaction
+
+		sle.fifo_queue_qty = fifo_qty
+		sle.fifo_stock_value = fifo_value
+		sle.fifo_valuation_rate = fifo_value / fifo_qty if fifo_qty else None
+		sle.balance_value_by_qty = (
+			sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None
+		)
+		sle.expected_qty_after_transaction = balance_qty
+
+		# set difference fields
+		sle.difference_in_qty = sle.qty_after_transaction - sle.expected_qty_after_transaction
+		sle.fifo_qty_diff = sle.qty_after_transaction - fifo_qty
+		sle.fifo_value_diff = sle.stock_value - fifo_value
+		sle.fifo_valuation_diff = (
+			sle.valuation_rate - sle.fifo_valuation_rate if sle.fifo_valuation_rate else None
+		)
+		sle.valuation_diff = (
+			sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None
+		)
+
+		if idx > 0:
+			sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
+			sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference
+
+	return sles
+
+
+def get_columns():
+	return [
+		{
+			"fieldname": "name",
+			"fieldtype": "Link",
+			"label": "Stock Ledger Entry",
+			"options": "Stock Ledger Entry",
+		},
+		{
+			"fieldname": "posting_date",
+			"fieldtype": "Date",
+			"label": "Posting Date",
+		},
+		{
+			"fieldname": "posting_time",
+			"fieldtype": "Time",
+			"label": "Posting Time",
+		},
+		{
+			"fieldname": "creation",
+			"fieldtype": "Datetime",
+			"label": "Creation",
+		},
+		{
+			"fieldname": "voucher_type",
+			"fieldtype": "Link",
+			"label": "Voucher Type",
+			"options": "DocType",
+		},
+		{
+			"fieldname": "voucher_no",
+			"fieldtype": "Dynamic Link",
+			"label": "Voucher No",
+			"options": "voucher_type",
+		},
+		{
+			"fieldname": "batch_no",
+			"fieldtype": "Link",
+			"label": "Batch",
+			"options": "Batch",
+		},
+		{
+			"fieldname": "actual_qty",
+			"fieldtype": "Float",
+			"label": "Qty Change",
+		},
+		{
+			"fieldname": "incoming_rate",
+			"fieldtype": "Float",
+			"label": "Incoming Rate",
+		},
+		{
+			"fieldname": "outgoing_rate",
+			"fieldtype": "Float",
+			"label": "Outgoing Rate",
+		},
+		{
+			"fieldname": "qty_after_transaction",
+			"fieldtype": "Float",
+			"label": "(A) Qty After Transaction",
+		},
+		{
+			"fieldname": "expected_qty_after_transaction",
+			"fieldtype": "Float",
+			"label": "(B) Expected Qty After Transaction",
+		},
+		{
+			"fieldname": "difference_in_qty",
+			"fieldtype": "Float",
+			"label": "A - B",
+		},
+		{
+			"fieldname": "stock_queue",
+			"fieldtype": "Data",
+			"label": "FIFO Queue",
+		},
+
+		{
+			"fieldname": "fifo_queue_qty",
+			"fieldtype": "Float",
+			"label": "(C) Total qty in queue",
+		},
+		{
+			"fieldname": "fifo_qty_diff",
+			"fieldtype": "Float",
+			"label": "A - C",
+		},
+		{
+			"fieldname": "stock_value",
+			"fieldtype": "Float",
+			"label": "(D) Balance Stock Value",
+		},
+		{
+			"fieldname": "fifo_stock_value",
+			"fieldtype": "Float",
+			"label": "(E) Balance Stock Value in Queue",
+		},
+		{
+			"fieldname": "fifo_value_diff",
+			"fieldtype": "Float",
+			"label": "D - E",
+		},
+
+		{
+			"fieldname": "stock_value_difference",
+			"fieldtype": "Float",
+			"label": "(F) Stock Value Difference",
+		},
+		{
+			"fieldname": "fifo_stock_diff",
+			"fieldtype": "Float",
+			"label": "(G) Stock Value difference (FIFO queue)",
+		},
+		{
+			"fieldname": "fifo_difference_diff",
+			"fieldtype": "Float",
+			"label": "F - G",
+		},
+		{
+			"fieldname": "valuation_rate",
+			"fieldtype": "Float",
+			"label": "(H) Valuation Rate",
+		},
+		{
+			"fieldname": "fifo_valuation_rate",
+			"fieldtype": "Float",
+			"label": "(I) Valuation Rate as per FIFO",
+		},
+
+		{
+			"fieldname": "fifo_valuation_diff",
+			"fieldtype": "Float",
+			"label": "H - I",
+		},
+		{
+			"fieldname": "balance_value_by_qty",
+			"fieldtype": "Float",
+			"label": "(J) Valuation = Value (D) ÷ Qty (A)",
+		},
+		{
+			"fieldname": "valuation_diff",
+			"fieldtype": "Float",
+			"label": "H - J",
+		},
+	]
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index d7fb5b2..1dcf863 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -41,6 +41,12 @@
 	("Total Stock Summary", {"group_by": "warehouse",}),
 	("Batch Item Expiry Status", {}),
 	("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
+	("Stock Ledger Invariant Check",
+		{
+			"warehouse": "_Test Warehouse - _TC",
+			"item": "_Test Item"
+		}
+	),
 ]
 
 OPTIONAL_FILTERS = {
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index d78632a..e95c0fc 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -1089,17 +1089,36 @@
 	allow_negative_stock = cint(allow_negative_stock) \
 		or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
 
-	if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock:
-		sle = get_future_sle_with_negative_qty(args)
-		if sle:
-			message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
-				abs(sle[0]["qty_after_transaction"]),
-				frappe.get_desk_link('Item', args.item_code),
-				frappe.get_desk_link('Warehouse', args.warehouse),
-				sle[0]["posting_date"], sle[0]["posting_time"],
-				frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"]))
+	if allow_negative_stock:
+		return
+	if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
+		return
 
-			frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+	neg_sle = get_future_sle_with_negative_qty(args)
+	if neg_sle:
+		message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+			abs(neg_sle[0]["qty_after_transaction"]),
+			frappe.get_desk_link('Item', args.item_code),
+			frappe.get_desk_link('Warehouse', args.warehouse),
+			neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
+			frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
+
+		frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+
+
+	if not args.batch_no:
+		return
+
+	neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
+	if neg_batch_sle:
+		message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+			abs(neg_batch_sle[0]["cumulative_total"]),
+			frappe.get_desk_link('Batch', args.batch_no),
+			frappe.get_desk_link('Warehouse', args.warehouse),
+			neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
+			frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
+		frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch")
+
 
 def get_future_sle_with_negative_qty(args):
 	return frappe.db.sql("""
@@ -1118,6 +1137,29 @@
 		limit 1
 	""", args, as_dict=1)
 
+
+def get_future_sle_with_negative_batch_qty(args):
+	return frappe.db.sql("""
+		with batch_ledger as (
+			select
+				posting_date, posting_time, voucher_type, voucher_no,
+				sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
+			from `tabStock Ledger Entry`
+			where
+				item_code = %(item_code)s
+				and warehouse = %(warehouse)s
+				and batch_no=%(batch_no)s
+				and is_cancelled = 0
+			order by posting_date, posting_time, creation
+		)
+		select * from batch_ledger
+		where
+			cumulative_total < 0.0
+			and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
+		limit 1
+	""", args, as_dict=1)
+
+
 def _round_off_if_near_zero(number: float, precision: int = 6) -> float:
 	""" Rounds off the number to zero only if number is close to zero for decimal
 		specified in precision. Precision defaults to 6.
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 72d8098..3b1ae3b 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -12,6 +12,7 @@
 
 
 class InvalidWarehouseCompany(frappe.ValidationError): pass
+class PendingRepostingError(frappe.ValidationError): pass
 
 def get_stock_value_from_bin(warehouse=None, item_code=None):
 	values = {}
@@ -417,3 +418,28 @@
 		{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
 	if reposting_in_progress:
 		frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
+
+def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool:
+	"""Check if there are pending reposting job till the specified posting date."""
+
+	filters = {
+		"docstatus": 1,
+		"status": ["in", ["Queued","In Progress", "Failed"]],
+		"posting_date": ["<=", posting_date],
+	}
+
+	reposting_pending =  frappe.db.exists("Repost Item Valuation", filters)
+	if reposting_pending and throw_error:
+		msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.")
+		frappe.msgprint(msg,
+				raise_exception=PendingRepostingError,
+				title="Stock Reposting Ongoing",
+				indicator="red",
+				primary_action={
+					"label": _("Show pending entries"),
+					"client_action": "erpnext.route_to_pending_reposts",
+					"args": filters,
+				}
+			)
+
+	return bool(reposting_pending)
diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json
index 14712f8..3ff7d02 100644
--- a/erpnext/support/doctype/issue/issue.json
+++ b/erpnext/support/doctype/issue/issue.json
@@ -24,12 +24,10 @@
   "service_level_section",
   "service_level_agreement",
   "response_by",
-  "response_by_variance",
   "reset_service_level_agreement",
   "cb",
   "agreement_status",
   "resolution_by",
-  "resolution_by_variance",
   "service_level_agreement_creation",
   "on_hold_since",
   "total_hold_time",
@@ -123,7 +121,6 @@
    "search_index": 1
   },
   {
-   "default": "Medium",
    "fieldname": "priority",
    "fieldtype": "Link",
    "in_list_view": 1,
@@ -319,22 +316,6 @@
    "label": "Via Customer Portal"
   },
   {
-   "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
-   "fieldname": "response_by_variance",
-   "fieldtype": "Duration",
-   "hide_seconds": 1,
-   "label": "Response By Variance",
-   "read_only": 1
-  },
-  {
-   "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
-   "fieldname": "resolution_by_variance",
-   "fieldtype": "Duration",
-   "hide_seconds": 1,
-   "label": "Resolution By Variance",
-   "read_only": 1
-  },
-  {
    "fieldname": "service_level_agreement_creation",
    "fieldtype": "Datetime",
    "hidden": 1,
@@ -391,12 +372,12 @@
    "read_only": 1
   },
   {
-   "default": "Ongoing",
+   "default": "First Response Due",
    "depends_on": "eval: doc.service_level_agreement",
    "fieldname": "agreement_status",
    "fieldtype": "Select",
    "label": "Service Level Agreement Status",
-   "options": "Ongoing\nFulfilled\nFailed",
+   "options": "First Response Due\nResolution Due\nFulfilled\nFailed",
    "read_only": 1
   },
   {
@@ -410,10 +391,11 @@
  "icon": "fa fa-ticket",
  "idx": 7,
  "links": [],
- "modified": "2021-06-10 03:22:27.098898",
+ "modified": "2021-11-24 13:13:10.276630",
  "modified_by": "Administrator",
  "module": "Support",
  "name": "Issue",
+ "naming_rule": "By \"Naming Series\" field",
  "owner": "Administrator",
  "permissions": [
   {
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 0dc3639..d5e5b78 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -87,11 +87,9 @@
 		if replicated_issue.service_level_agreement:
 			replicated_issue.service_level_agreement_creation = now_datetime()
 			replicated_issue.service_level_agreement = None
-			replicated_issue.agreement_status = "Ongoing"
+			replicated_issue.agreement_status = "First Response Due"
 			replicated_issue.response_by = None
-			replicated_issue.response_by_variance = None
 			replicated_issue.resolution_by = None
-			replicated_issue.resolution_by_variance = None
 			replicated_issue.reset_issue_metrics()
 
 		frappe.get_doc(replicated_issue).insert()
diff --git a/erpnext/support/doctype/issue/issue_list.js b/erpnext/support/doctype/issue/issue_list.js
index e04498e..5bfecb0 100644
--- a/erpnext/support/doctype/issue/issue_list.js
+++ b/erpnext/support/doctype/issue/issue_list.js
@@ -18,7 +18,6 @@
 	},
 	get_indicator: function(doc) {
 		if (doc.status === 'Open') {
-			if (!doc.priority) doc.priority = 'Medium';
 			const color = {
 				'Low': 'yellow',
 				'Medium': 'orange',
diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py
index ab9a444b..14cec46 100644
--- a/erpnext/support/doctype/issue/test_issue.py
+++ b/erpnext/support/doctype/issue/test_issue.py
@@ -1,10 +1,10 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
 # See license.txt
 
-import datetime
 import unittest
 
 import frappe
+from frappe import _
 from frappe.core.doctype.user_permission.test_user_permission import create_user
 from frappe.utils import flt, get_datetime
 
@@ -83,30 +83,6 @@
 
 		self.assertEqual(issue.agreement_status, 'Fulfilled')
 
-	def test_issue_metrics(self):
-		creation = get_datetime("2020-03-04 4:00")
-
-		issue = make_issue(creation, index=1)
-		create_communication(issue.name, "test@example.com", "Received", creation)
-
-		creation = get_datetime("2020-03-04 4:15")
-		create_communication(issue.name, "test@admin.com", "Sent", creation)
-
-		creation = get_datetime("2020-03-04 5:00")
-		create_communication(issue.name, "test@example.com", "Received", creation)
-
-		creation = get_datetime("2020-03-04 5:05")
-		create_communication(issue.name, "test@admin.com", "Sent", creation)
-
-		frappe.flags.current_time = get_datetime("2020-03-04 5:05")
-		issue.reload()
-		issue.status = 'Closed'
-		issue.save()
-
-		self.assertEqual(issue.avg_response_time, 600)
-		self.assertEqual(issue.resolution_time, 3900)
-		self.assertEqual(issue.user_resolution_time, 1200)
-
 	def test_hold_time_on_replied(self):
 		creation = get_datetime("2020-03-04 4:00")
 
@@ -142,6 +118,142 @@
 		issue.reload()
 		self.assertEqual(flt(issue.total_hold_time, 2), 2700)
 
+	def test_issue_close_after_on_hold(self):
+		frappe.flags.current_time = get_datetime("2021-11-01 19:00")
+
+		issue = make_issue(frappe.flags.current_time, index=1)
+		create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
+
+		# send a reply within SLA
+		frappe.flags.current_time = get_datetime("2021-11-02 11:00")
+		create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
+
+		issue.reload()
+		issue.status = 'Replied'
+		issue.save()
+
+		self.assertEqual(issue.on_hold_since, frappe.flags.current_time)
+
+		# close the issue after being on hold for 20 days
+		frappe.flags.current_time = get_datetime("2021-11-22 01:00")
+		issue.status = 'Closed'
+		issue.save()
+
+		self.assertEqual(issue.resolution_by, get_datetime('2021-11-22 06:00:00'))
+		self.assertEqual(issue.resolution_date, get_datetime('2021-11-22 01:00:00'))
+		self.assertEqual(issue.agreement_status, 'Fulfilled')
+
+	def test_issue_open_after_closed(self):
+
+		# Created on -> 1 pm, Response Time -> 4 hrs, Resolution Time -> 6 hrs
+		frappe.flags.current_time = get_datetime("2021-11-01 13:00")
+		issue = make_issue(frappe.flags.current_time, index=1, issue_type='Critical') # Applies 24hr working time SLA
+		create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
+		self.assertEquals(issue.agreement_status, 'First Response Due')
+		self.assertEquals(issue.response_by, get_datetime("2021-11-01 17:00"))
+		self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 19:00"))
+
+		# Replied on → 2 pm
+		frappe.flags.current_time = get_datetime("2021-11-01 14:00")
+		create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
+		issue.reload()
+		issue.status = 'Replied'
+		issue.save()
+		self.assertEquals(issue.agreement_status, 'Resolution Due')
+		self.assertEquals(issue.on_hold_since, frappe.flags.current_time)
+		self.assertEquals(issue.first_responded_on, frappe.flags.current_time)
+
+		# Customer Replied → 3 pm
+		frappe.flags.current_time = get_datetime("2021-11-01 15:00")
+		create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
+		issue.reload()
+		self.assertEquals(issue.status, 'Open')
+		# Hold Time + 1 Hrs
+		self.assertEquals(issue.total_hold_time, 3600)
+		# Resolution By should increase by one hrs
+		self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 20:00"))
+
+		# Replied on → 4 pm, Open → 1 hr, Resolution Due → 8 pm
+		frappe.flags.current_time = get_datetime("2021-11-01 16:00")
+		create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
+		issue.reload()
+		issue.status = 'Replied'
+		issue.save()
+		self.assertEquals(issue.agreement_status, 'Resolution Due')
+
+		# Customer Closed → 10 pm
+		frappe.flags.current_time = get_datetime("2021-11-01 22:00")
+		issue.status = 'Closed'
+		issue.save()
+		# Hold Time + 6 Hrs
+		self.assertEquals(issue.total_hold_time, 3600 + 21600)
+		# Resolution By should increase by 6 hrs
+		self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 02:00"))
+		self.assertEquals(issue.agreement_status, 'Fulfilled')
+		self.assertEquals(issue.resolution_date, frappe.flags.current_time)
+
+		# Customer Open → 3 am i.e after resolution by is crossed
+		frappe.flags.current_time = get_datetime("2021-11-02 03:00")
+		create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
+		issue.reload()
+		# Since issue was Resolved, Resolution By should be increased by 5 hrs (3am - 10pm)
+		self.assertEquals(issue.total_hold_time, 3600 + 21600 + 18000)
+		# Resolution By should increase by 5 hrs
+		self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00"))
+		self.assertEquals(issue.agreement_status, 'Resolution Due')
+		self.assertFalse(issue.resolution_date)
+
+		# We Closed → 4 am, SLA should be Fulfilled
+		frappe.flags.current_time = get_datetime("2021-11-02 04:00")
+		issue.status = 'Closed'
+		issue.save()
+		self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00"))
+		self.assertEquals(issue.agreement_status, 'Fulfilled')
+		self.assertEquals(issue.resolution_date, frappe.flags.current_time)
+
+	def test_recording_of_assignment_on_first_reponse_failure(self):
+		from frappe.desk.form.assign_to import add as add_assignment
+
+		frappe.flags.current_time = get_datetime("2021-11-01 19:00")
+
+		issue = make_issue(frappe.flags.current_time, index=1)
+		create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
+		add_assignment({
+			'doctype': issue.doctype,
+			'name': issue.name,
+			'assign_to': ['test@admin.com']
+		})
+		issue.reload()
+
+		# send a reply failing response SLA
+		frappe.flags.current_time = get_datetime("2021-11-02 15:00")
+		create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
+
+		# assert if a new timeline item has been added
+		# to record the assignment
+		comment = frappe.db.exists('Comment', {
+			'reference_doctype': 'Issue',
+			'reference_name': issue.name,
+			'comment_type': 'Assigned',
+			'content': _('First Response SLA Failed by {}').format('test')
+		})
+		self.assertTrue(comment)
+
+	def test_agreement_status_on_response(self):
+		frappe.flags.current_time = get_datetime("2021-11-01 19:00")
+
+		issue = make_issue(frappe.flags.current_time, index=1)
+		create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
+		self.assertTrue(issue.status == 'Open')
+
+		# send a reply within response SLA
+		frappe.flags.current_time = get_datetime("2021-11-02 11:00")
+		create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time)
+
+		issue.reload()
+		self.assertEquals(issue.first_responded_on, frappe.flags.current_time)
+		self.assertEquals(issue.agreement_status, 'Resolution Due')
+
 class TestFirstResponseTime(TestSetUp):
 	# working hours used in all cases: Mon-Fri, 10am to 6pm
 	# all dates are in the mm-dd-yyyy format
@@ -355,12 +467,18 @@
 def create_issue_and_communication(issue_creation, first_responded_on):
 	issue = make_issue(issue_creation, index=1)
 	sender = create_user("test@admin.com")
+	frappe.flags.current_time = first_responded_on
 	create_communication(issue.name, sender.email, "Sent", first_responded_on)
 	issue.reload()
 
 	return issue
 
 def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None):
+	if issue_type and not frappe.db.exists('Issue Type', issue_type):
+		doc = frappe.new_doc('Issue Type')
+		doc.name = issue_type
+		doc.insert()
+
 	issue = frappe.get_doc({
 		"doctype": "Issue",
 		"subject": "Service Level Agreement Issue {0}".format(index),
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
index ae2080c..bfbffe2 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
@@ -22,10 +22,41 @@
 	refresh: function(frm) {
 		frm.trigger('fetch_status_fields');
 		frm.trigger('toggle_resolution_fields');
+		frm.trigger('default_service_level_agreement');
+		frm.trigger('entity');
+	},
+
+	default_service_level_agreement: function(frm) {
+		const field = frm.get_field('default_service_level_agreement');
+		if (frm.doc.default_service_level_agreement) {
+			field.set_description(__('SLA will be applied on every {0}', [frm.doc.document_type]));
+		} else {
+			field.set_description(__('Enable to apply SLA on every {0}', [frm.doc.document_type]));
+		}
 	},
 
 	document_type: function(frm) {
 		frm.trigger('fetch_status_fields');
+		frm.trigger('default_service_level_agreement');
+	},
+
+	entity_type: function(frm) {
+		frm.set_value('entity', undefined);
+	},
+
+	entity: function(frm) {
+		const field = frm.get_field('entity');
+		if (frm.doc.entity) {
+			const and_descendants = frm.doc.entity_type != 'Customer' ? ' ' + __('or its descendants') : '';
+			field.set_description(
+				__('SLA will be applied if {1} is set as {2}{3}', [
+					frm.doc.document_type, frm.doc.entity_type,
+					frm.doc.entity, and_descendants
+				])
+			);
+		} else {
+			field.set_description('');
+		}
 	},
 
 	fetch_status_fields: function(frm) {
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
index 5f470aa..1698e23 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
@@ -6,22 +6,17 @@
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
-  "enabled",
-  "section_break_2",
   "document_type",
-  "default_service_level_agreement",
   "default_priority",
   "column_break_2",
   "service_level",
-  "holiday_list",
-  "entity_section",
-  "entity_type",
-  "column_break_10",
-  "entity",
+  "enabled",
   "filters_section",
-  "condition",
+  "default_service_level_agreement",
+  "entity_type",
+  "entity",
   "column_break_15",
-  "condition_description",
+  "condition",
   "agreement_details_section",
   "start_date",
   "column_break_7",
@@ -31,8 +26,10 @@
   "priorities",
   "status_details",
   "sla_fulfilled_on",
+  "column_break_22",
   "pause_sla_on",
   "support_and_resolution_section_break",
+  "holiday_list",
   "support_and_resolution"
  ],
  "fields": [
@@ -42,7 +39,8 @@
    "in_list_view": 1,
    "in_standard_filter": 1,
    "label": "Service Level Name",
-   "reqd": 1
+   "reqd": 1,
+   "set_only_once": 1
   },
   {
    "fieldname": "holiday_list",
@@ -56,10 +54,10 @@
    "fieldtype": "Column Break"
   },
   {
-   "depends_on": "eval: !doc.default_service_level_agreement",
+   "depends_on": "eval: doc.document_type",
    "fieldname": "agreement_details_section",
    "fieldtype": "Section Break",
-   "label": "Agreement Details"
+   "label": "Valid From"
   },
   {
    "fieldname": "start_date",
@@ -72,7 +70,6 @@
    "fieldtype": "Column Break"
   },
   {
-   "depends_on": "eval: !doc.default_service_level_agreement",
    "fieldname": "end_date",
    "fieldtype": "Date",
    "label": "End Date"
@@ -80,7 +77,7 @@
   {
    "fieldname": "response_and_resolution_time_section",
    "fieldtype": "Section Break",
-   "label": "Response and Resolution Time"
+   "label": "Response and Resolution"
   },
   {
    "fieldname": "support_and_resolution_section_break",
@@ -90,6 +87,7 @@
   {
    "fieldname": "support_and_resolution",
    "fieldtype": "Table",
+   "label": "Working Hours",
    "options": "Service Day",
    "reqd": 1
   },
@@ -101,10 +99,7 @@
    "reqd": 1
   },
   {
-   "fieldname": "column_break_10",
-   "fieldtype": "Column Break"
-  },
-  {
+   "depends_on": "eval: !doc.default_service_level_agreement",
    "fieldname": "entity",
    "fieldtype": "Dynamic Link",
    "in_list_view": 1,
@@ -114,11 +109,6 @@
   },
   {
    "depends_on": "eval: !doc.default_service_level_agreement",
-   "fieldname": "entity_section",
-   "fieldtype": "Section Break",
-   "label": "Entity"
-  },
-  {
    "fieldname": "entity_type",
    "fieldtype": "Select",
    "in_standard_filter": 1,
@@ -126,11 +116,6 @@
    "options": "\nCustomer\nCustomer Group\nTerritory"
   },
   {
-   "fieldname": "section_break_2",
-   "fieldtype": "Section Break",
-   "hide_border": 1
-  },
-  {
    "default": "0",
    "fieldname": "default_service_level_agreement",
    "fieldtype": "Check",
@@ -152,7 +137,7 @@
   {
    "fieldname": "document_type",
    "fieldtype": "Link",
-   "label": "Document Type",
+   "label": "Apply On",
    "options": "DocType",
    "reqd": 1,
    "set_only_once": 1
@@ -164,6 +149,7 @@
    "label": "Enabled"
   },
   {
+   "depends_on": "document_type",
    "fieldname": "status_details",
    "fieldtype": "Section Break",
    "label": "Status Details"
@@ -182,28 +168,31 @@
    "label": "Apply SLA for Resolution Time"
   },
   {
+   "depends_on": "document_type",
    "fieldname": "filters_section",
    "fieldtype": "Section Break",
-   "label": "Assignment Condition"
+   "label": "Assignment Conditions"
   },
   {
    "fieldname": "column_break_15",
    "fieldtype": "Column Break"
   },
   {
+   "depends_on": "eval: !doc.default_service_level_agreement",
+   "description": "Simple Python Expression, Example: doc.status == 'Open' and doc.issue_type == 'Bug'",
    "fieldname": "condition",
    "fieldtype": "Code",
    "label": "Condition",
-   "options": "Python"
+   "max_height": "7rem",
+   "options": "PythonExpression"
   },
   {
-   "fieldname": "condition_description",
-   "fieldtype": "HTML",
-   "options": "<p><strong>Condition Examples:</strong></p>\n<pre>doc.status==\"Open\"<br>doc.due_date==nowdate()<br>doc.total &gt; 40000\n</pre>"
+   "fieldname": "column_break_22",
+   "fieldtype": "Column Break"
   }
  ],
  "links": [],
- "modified": "2021-10-02 11:32:55.556024",
+ "modified": "2021-11-26 15:45:33.289911",
  "modified_by": "Administrator",
  "module": "Support",
  "name": "Service Level Agreement",
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
index 5f8f83d..c94700b 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
@@ -10,7 +10,6 @@
 from frappe.model.document import Document
 from frappe.utils import (
 	add_to_date,
-	cint,
 	get_datetime,
 	get_datetime_str,
 	get_link_to_form,
@@ -22,6 +21,7 @@
 	time_diff_in_seconds,
 	to_timedelta,
 )
+from frappe.utils.nestedset import get_ancestors_of
 from frappe.utils.safe_exec import get_safe_globals
 
 from erpnext.support.doctype.issue.issue import get_holidays
@@ -248,7 +248,7 @@
 
 	customer = doc.get('customer')
 	or_filters.append(
-		["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]]
+		["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)]
 	)
 
 	default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]]
@@ -275,11 +275,23 @@
 	return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))}
 
 def get_customer_group(customer):
-	return frappe.db.get_value("Customer", customer, "customer_group") if customer else None
+	customer_groups = []
+	customer_group = frappe.db.get_value("Customer", customer, "customer_group") if customer else None
+	if customer_group:
+		ancestors = get_ancestors_of("Customer Group", customer_group)
+		customer_groups = [customer_group] + ancestors
+
+	return customer_groups
 
 
 def get_customer_territory(customer):
-	return frappe.db.get_value("Customer", customer, "territory") if customer else None
+	customer_territories = []
+	customer_territory = frappe.db.get_value("Customer", customer, "territory") if customer else None
+	if customer_territory:
+		ancestors = get_ancestors_of("Territory", customer_territory)
+		customer_territories = [customer_territory] + ancestors
+
+	return customer_territories
 
 
 @frappe.whitelist()
@@ -299,7 +311,7 @@
 	if customer:
 		# Include SLA with No Entity and Entity Type
 		or_filters.append(
-			["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), ""]]
+			["Service Level Agreement", "entity", "in", [""] + [customer] + get_customer_group(customer) + get_customer_territory(customer)]
 		)
 
 	return {
@@ -337,84 +349,135 @@
 
 def apply(doc, method=None):
 	# Applies SLA to document on validate
-	if frappe.flags.in_patch or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_setup_wizard or \
-		doc.doctype not in get_documents_with_active_service_level_agreement():
+	if (
+		frappe.flags.in_patch
+		or frappe.flags.in_migrate
+		or frappe.flags.in_install
+		or frappe.flags.in_setup_wizard
+		or doc.doctype not in get_documents_with_active_service_level_agreement()
+	):
 		return
 
-	service_level_agreement = get_active_service_level_agreement_for(doc)
+	sla = get_active_service_level_agreement_for(doc)
 
-	if not service_level_agreement:
+	if not sla:
 		return
 
-	set_sla_properties(doc, service_level_agreement)
+	process_sla(doc, sla)
 
 
-def set_sla_properties(doc, service_level_agreement):
-	if frappe.db.exists(doc.doctype, doc.name):
-		from_db = frappe.get_doc(doc.doctype, doc.name)
-	else:
-		from_db = frappe._dict({})
-
-	meta = frappe.get_meta(doc.doctype)
-
-	if meta.has_field("customer") and service_level_agreement.customer and doc.get("customer") and \
-		not service_level_agreement.customer == doc.get("customer"):
-		frappe.throw(_("Service Level Agreement {0} is specific to Customer {1}").format(service_level_agreement.name,
-			service_level_agreement.customer))
-
-	doc.service_level_agreement = service_level_agreement.name
-	doc.priority = doc.get("priority") or service_level_agreement.default_priority
-	priority = get_priority(doc)
+def process_sla(doc, sla):
 
 	if not doc.creation:
 		doc.creation = now_datetime(doc.get("owner"))
-
-		if meta.has_field("service_level_agreement_creation"):
+		if doc.meta.has_field("service_level_agreement_creation"):
 			doc.service_level_agreement_creation = now_datetime(doc.get("owner"))
 
+	doc.service_level_agreement = sla.name
+	doc.priority = doc.get("priority") or sla.default_priority
+
+	handle_status_change(doc, sla.apply_sla_for_resolution)
+	update_response_and_resolution_metrics(doc, sla.apply_sla_for_resolution)
+	update_agreement_status(doc, sla.apply_sla_for_resolution)
+
+
+def handle_status_change(doc, apply_sla_for_resolution):
+	now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
+	prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status')
+
+	hold_statuses = get_hold_statuses(doc.service_level_agreement)
+	fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement)
+
+	def is_hold_status(status):
+		return status in hold_statuses
+
+	def is_fulfilled_status(status):
+		return status in fulfillment_statuses
+
+	def is_open_status(status):
+		return status not in hold_statuses and status not in fulfillment_statuses
+
+	def set_first_response():
+		if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
+			doc.first_responded_on = now_time
+			if get_datetime(doc.get('first_responded_on')) > get_datetime(doc.get('response_by')):
+				record_assigned_users_on_failure(doc)
+
+	def calculate_hold_hours():
+		# In case issue was closed and after few days it has been opened
+		# The hold time should be calculated from resolution_date
+
+		on_hold_since = doc.resolution_date or doc.on_hold_since
+		if on_hold_since:
+			current_hold_hours = time_diff_in_seconds(now_time, on_hold_since)
+			doc.total_hold_time = (doc.total_hold_time or 0) + current_hold_hours
+		doc.on_hold_since = None
+
+	if ((is_open_status(prev_status) and not is_open_status(doc.status)) or doc.flags.on_first_reply):
+		set_first_response()
+
+	# Open to Replied
+	if is_open_status(prev_status) and is_hold_status(doc.status):
+		# Issue is on hold -> Set on_hold_since
+		doc.on_hold_since = now_time
+		reset_expected_response_and_resolution(doc)
+
+	# Replied to Open
+	if is_hold_status(prev_status) and is_open_status(doc.status):
+		# Issue was on hold -> Calculate Total Hold Time
+		calculate_hold_hours()
+		# Issue is open -> reset resolution_date
+		reset_resolution_metrics(doc)
+
+	# Open to Closed
+	if is_open_status(prev_status) and is_fulfilled_status(doc.status):
+		# Issue is closed -> Set resolution_date
+		doc.resolution_date = now_time
+		set_resolution_time(doc)
+
+	# Closed to Open
+	if is_fulfilled_status(prev_status) and is_open_status(doc.status):
+		# Issue was closed -> Calculate Total Hold Time from resolution_date
+		calculate_hold_hours()
+		# Issue is open -> reset resolution_date
+		reset_resolution_metrics(doc)
+
+	# Closed to Replied
+	if is_fulfilled_status(prev_status) and is_hold_status(doc.status):
+		# Issue was closed -> Calculate Total Hold Time from resolution_date
+		calculate_hold_hours()
+		# Issue is on hold -> Set on_hold_since
+		doc.on_hold_since = now_time
+		reset_expected_response_and_resolution(doc)
+
+	# Replied to Closed
+	if is_hold_status(prev_status) and is_fulfilled_status(doc.status):
+		# Issue was on hold -> Calculate Total Hold Time
+		calculate_hold_hours()
+		# Issue is closed -> Set resolution_date
+		if apply_sla_for_resolution:
+			doc.resolution_date = now_time
+			set_resolution_time(doc)
+
+
+def get_fulfillment_statuses(service_level_agreement):
+	return [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={
+		"parent": service_level_agreement
+	}, fields=["status"])]
+
+
+def get_hold_statuses(service_level_agreement):
+	return [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={
+		"parent": service_level_agreement
+	}, fields=["status"])]
+
+
+def update_response_and_resolution_metrics(doc, apply_sla_for_resolution):
+	priority = get_response_and_resolution_duration(doc)
 	start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
-
-	set_response_by_and_variance(doc, meta, start_date_time, priority)
-	if service_level_agreement.apply_sla_for_resolution:
-		set_resolution_by_and_variance(doc, meta, start_date_time, priority)
-
-	update_status(doc, from_db, meta)
-
-
-def update_status(doc, from_db, meta):
-	if meta.has_field("status"):
-		if meta.has_field("first_responded_on") and doc.status != "Open" and \
-			from_db.status == "Open" and not doc.first_responded_on:
-			doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner"))
-
-		if meta.has_field("service_level_agreement") and doc.service_level_agreement:
-			# mark sla status as fulfilled based on the configuration
-			fulfillment_statuses = [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={
-				"parent": doc.service_level_agreement
-			}, fields=["status"])]
-
-			if doc.status in fulfillment_statuses and from_db.status not in fulfillment_statuses:
-				apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement,
-					"apply_sla_for_resolution")
-
-				if apply_sla_for_resolution and meta.has_field("resolution_date"):
-					doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner"))
-
-				if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing":
-					set_service_level_agreement_variance(doc.doctype, doc.name)
-					update_agreement_status(doc, meta)
-
-				if apply_sla_for_resolution:
-					set_resolution_time(doc, meta)
-					set_user_resolution_time(doc, meta)
-
-		if doc.status == "Open" and from_db.status != "Open":
-			# if no date, it should be set as None and not a blank string "", as per mysql strict config
-			# enable SLA and variance on Reopen
-			reset_metrics(doc, meta)
-			set_service_level_agreement_variance(doc.doctype, doc.name)
-
-	handle_hold_time(doc, meta, from_db.status)
+	set_response_by(doc, start_date_time, priority)
+	if apply_sla_for_resolution:
+		set_resolution_by(doc, start_date_time, priority)
 
 
 def get_expected_time_for(parameter, service_level, start_date_time):
@@ -485,37 +548,13 @@
 	return support_days
 
 
-def set_service_level_agreement_variance(doctype, doc=None):
+def set_resolution_time(doc):
+	start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
+	if doc.meta.has_field("resolution_time"):
+		doc.resolution_time = time_diff_in_seconds(doc.resolution_date, start_date_time)
 
-	filters = {"status": "Open", "agreement_status": "Ongoing"}
-
-	if doc:
-		filters = {"name": doc}
-
-	for entry in frappe.get_all(doctype, filters=filters):
-		current_doc = frappe.get_doc(doctype, entry.name)
-		current_time = frappe.flags.current_time or now_datetime(current_doc.get("owner"))
-		apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", current_doc.service_level_agreement,
-			"apply_sla_for_resolution")
-
-		if not current_doc.first_responded_on: # first_responded_on set when first reply is sent to customer
-			variance = round(time_diff_in_seconds(current_doc.response_by, current_time), 2)
-			frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False)
-
-			if variance < 0:
-				frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False)
-
-		if apply_sla_for_resolution and not current_doc.get("resolution_date"): # resolution_date set when issue has been closed
-			variance = round(time_diff_in_seconds(current_doc.resolution_by, current_time), 2)
-			frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False)
-
-			if variance < 0:
-				frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False)
-
-
-def set_user_resolution_time(doc, meta):
 	# total time taken by a user to close the issue apart from wait_time
-	if not meta.has_field("user_resolution_time"):
+	if not doc.meta.has_field("user_resolution_time"):
 		return
 
 	communications = frappe.get_all("Communication", filters={
@@ -531,7 +570,7 @@
 				pending_time.append(wait_time)
 
 	total_pending_time = sum(pending_time)
-	resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, doc.creation)
+	resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, start_date_time)
 	doc.user_resolution_time = resolution_time_in_secs - total_pending_time
 
 
@@ -548,12 +587,12 @@
 			frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement))
 
 
-def get_priority(doc):
-	service_level_agreement = frappe.get_doc("Service Level Agreement", doc.service_level_agreement)
-	priority = service_level_agreement.get_service_level_agreement_priority(doc.priority)
+def get_response_and_resolution_duration(doc):
+	sla = frappe.get_doc("Service Level Agreement", doc.service_level_agreement)
+	priority = sla.get_service_level_agreement_priority(doc.priority)
 	priority.update({
-		"support_and_resolution": service_level_agreement.support_and_resolution,
-		"holiday_list": service_level_agreement.holiday_list
+		"support_and_resolution": sla.support_and_resolution,
+		"holiday_list": sla.holiday_list
 	})
 	return priority
 
@@ -572,120 +611,102 @@
 	}).insert(ignore_permissions=True)
 
 	doc.service_level_agreement_creation = now_datetime(doc.get("owner"))
-	doc.set_response_and_resolution_time(priority=doc.priority, service_level_agreement=doc.service_level_agreement)
-	doc.agreement_status = "Ongoing"
 	doc.save()
 
 
-def reset_metrics(doc, meta):
-	if meta.has_field("resolution_date"):
+def reset_resolution_metrics(doc):
+	if doc.meta.has_field("resolution_date"):
 		doc.resolution_date = None
 
-	if not meta.has_field("resolution_time"):
+	if doc.meta.has_field("resolution_time"):
 		doc.resolution_time = None
 
-	if not meta.has_field("user_resolution_time"):
+	if doc.meta.has_field("user_resolution_time"):
 		doc.user_resolution_time = None
 
-	if meta.has_field("agreement_status"):
-		doc.agreement_status = "Ongoing"
-
-
-def set_resolution_time(doc, meta):
-	# total time taken from issue creation to closing
-	if not meta.has_field("resolution_time"):
-		return
-
-	doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation)
+	if doc.meta.has_field("agreement_status"):
+		doc.agreement_status = "First Response Due"
 
 
 # called via hooks on communication update
-def update_hold_time(doc, status):
+def on_communication_update(doc, status):
+	if doc.communication_type == "Comment":
+		return
+
 	parent = get_parent_doc(doc)
 	if not parent:
 		return
 
-	if doc.communication_type == "Comment":
+	if not parent.meta.has_field('service_level_agreement'):
 		return
 
-	status_field = parent.meta.get_field("status")
-	if status_field:
-		options = (status_field.options or "").splitlines()
+	if (
+		doc.sent_or_received == "Received" # a reply is received
+		and parent.get('status') == 'Open' # issue status is set as open from communication.py
+		and parent.get_doc_before_save()
+		and parent.get('status') != parent._doc_before_save.get('status') # status changed
+	):
+		# undo the status change in db
+		# since prev status is fetched from db
+		frappe.db.set_value(
+			parent.doctype, parent.name,
+			'status', parent._doc_before_save.get('status'),
+			update_modified=False
+		)
 
-		# if status has a "Replied" option, then handle hold time
-		if ("Replied" in options) and doc.sent_or_received == "Received":
-			meta = frappe.get_meta(parent.doctype)
-			handle_hold_time(parent, meta, 'Replied')
+	elif (
+		doc.sent_or_received == "Sent" # a reply is sent
+		and parent.get('first_responded_on') # first_responded_on is set from communication.py
+		and parent.get_doc_before_save()
+		and not parent._doc_before_save.get('first_responded_on') # first_responded_on was not set
+	):
+		# reset first_responded_on since it will be handled/set later on
+		parent.first_responded_on = None
+		parent.flags.on_first_reply = True
+
+	else:
+		return
+
+	for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution')
+
+	handle_status_change(parent, for_resolution)
+	update_response_and_resolution_metrics(parent, for_resolution)
+	update_agreement_status(parent, for_resolution)
+
+	parent.save()
 
 
-def handle_hold_time(doc, meta, status):
-	if meta.has_field("service_level_agreement") and doc.service_level_agreement:
-		# set response and resolution variance as None as the issue is on Hold for status as Replied
-		hold_statuses = [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={
-				"parent": doc.service_level_agreement
-			}, fields=["status"])]
-
-		if not hold_statuses:
-			return
-
-		if meta.has_field("status") and doc.status in hold_statuses and status not in hold_statuses:
-			apply_hold_status(doc, meta)
-
-		# calculate hold time when status is changed from any hold status to any non-hold status
-		if meta.has_field("status") and doc.status not in hold_statuses and status in hold_statuses:
-			reset_hold_status_and_update_hold_time(doc, meta)
+def reset_expected_response_and_resolution(doc):
+	if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
+		doc.response_by = None
+	if doc.meta.has_field("resolution_by") and not doc.get('resolution_date'):
+		doc.resolution_by = None
 
 
-def apply_hold_status(doc, meta):
-	update_values = {'on_hold_since': frappe.flags.current_time or now_datetime(doc.get("owner"))}
-
-	if meta.has_field("first_responded_on") and not doc.first_responded_on:
-		update_values['response_by'] = None
-		update_values['response_by_variance'] = 0
-
-	update_values['resolution_by'] = None
-	update_values['resolution_by_variance'] = 0
-
-	doc.db_set(update_values)
+def set_response_by(doc, start_date_time, priority):
+	if doc.meta.has_field("response_by"):
+		doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
+		if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time') and not doc.get('first_responded_on'):
+			doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time')))
 
 
-def reset_hold_status_and_update_hold_time(doc, meta):
-	hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0
-	now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
-	last_hold_time = 0
-	update_values = {}
+def set_resolution_by(doc, start_date_time, priority):
+	if doc.meta.has_field("resolution_by"):
+		doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
+		if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'):
+			doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time')))
 
-	if meta.has_field("on_hold_since") and doc.on_hold_since:
-		# last_hold_time will be added to the sla variables
-		last_hold_time = time_diff_in_seconds(now_time, doc.on_hold_since)
-		update_values['total_hold_time'] = hold_time + last_hold_time
 
-	# re-calculate SLA variables after issue changes from any hold status to any non-hold status
-	start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
-	priority = get_priority(doc)
-	now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
-
-	# add hold time to response by variance
-	if meta.has_field("first_responded_on") and not doc.first_responded_on:
-		response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
-		response_by = add_to_date(response_by, seconds=round(last_hold_time))
-		response_by_variance = round(time_diff_in_seconds(response_by, now_time))
-
-		update_values['response_by'] = response_by
-		update_values['response_by_variance'] = response_by_variance + last_hold_time
-
-	# add hold time to resolution by variance
-	if frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, "apply_sla_for_resolution"):
-		resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
-		resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time))
-		resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time))
-
-		update_values['resolution_by'] = resolution_by
-		update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time
-
-	update_values['on_hold_since'] = None
-
-	doc.db_set(update_values)
+def record_assigned_users_on_failure(doc):
+	assigned_users = doc.get_assigned_users()
+	if assigned_users:
+		from frappe.utils import get_fullname
+		assigned_users = ', '.join((get_fullname(user) for user in assigned_users))
+		message = _('First Response SLA Failed by {}').format(assigned_users)
+		doc.add_comment(
+			comment_type='Assigned',
+			text=message
+		)
 
 
 def get_service_level_agreement_fields():
@@ -715,16 +736,10 @@
 			"read_only": 1
 		},
 		{
-			"fieldname": "response_by_variance",
-			"fieldtype": "Duration",
-			"hide_seconds": 1,
-			"label": "Response By Variance",
-			"read_only": 1
-		},
-		{
 			"fieldname": "first_responded_on",
 			"fieldtype": "Datetime",
 			"label": "First Responded On",
+			"no_copy": 1,
 			"read_only": 1
 		},
 		{
@@ -746,11 +761,11 @@
 			"read_only": 1
 		},
 		{
-			"default": "Ongoing",
+			"default": "First Response Due",
 			"fieldname": "agreement_status",
 			"fieldtype": "Select",
 			"label": "Service Level Agreement Status",
-			"options": "Ongoing\nFulfilled\nFailed",
+			"options": "First Response Due\nResolution Due\nFulfilled\nFailed",
 			"read_only": 1
 		},
 		{
@@ -760,13 +775,6 @@
 			"read_only": 1
 		},
 		{
-			"fieldname": "resolution_by_variance",
-			"fieldtype": "Duration",
-			"hide_seconds": 1,
-			"label": "Resolution By Variance",
-			"read_only": 1
-		},
-		{
 			"fieldname": "service_level_agreement_creation",
 			"fieldtype": "Datetime",
 			"hidden": 1,
@@ -786,43 +794,28 @@
 
 def update_agreement_status_on_custom_status(doc):
 	# Update Agreement Fulfilled status using Custom Scripts for Custom Status
-
-	meta = frappe.get_meta(doc.doctype)
-	now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
-	if meta.has_field("first_responded_on") and not doc.first_responded_on:
-		# first_responded_on set when first reply is sent to customer
-		doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2)
-
-	if meta.has_field("resolution_date") and not doc.resolution_date:
-		# resolution_date set when issue has been closed
-		doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2)
-
-	if meta.has_field("agreement_status"):
-		doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed"
+	update_agreement_status(doc)
 
 
-def update_agreement_status(doc, meta):
-	if meta.has_field("service_level_agreement") and meta.has_field("agreement_status") and \
-		doc.service_level_agreement and doc.agreement_status == "Ongoing":
-
-		apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement,
-			"apply_sla_for_resolution")
-
+def update_agreement_status(doc, apply_sla_for_resolution):
+	if (doc.meta.has_field("agreement_status")):
 		# if SLA is applied for resolution check for response and resolution, else only response
 		if apply_sla_for_resolution:
-			if meta.has_field("response_by_variance") and meta.has_field("resolution_by_variance"):
-				if cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0 or \
-					cint(frappe.db.get_value(doc.doctype, doc.name, "resolution_by_variance")) < 0:
-
-					doc.agreement_status = "Failed"
-				else:
-					doc.agreement_status = "Fulfilled"
-		else:
-			if meta.has_field("response_by_variance") and \
-				cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0:
-				doc.agreement_status = "Failed"
-			else:
+			if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
+				doc.agreement_status = "First Response Due"
+			elif doc.meta.has_field("resolution_date") and not doc.get('resolution_date'):
+				doc.agreement_status = "Resolution Due"
+			elif get_datetime(doc.get('resolution_date')) <= get_datetime(doc.get('resolution_by')):
 				doc.agreement_status = "Fulfilled"
+			else:
+				doc.agreement_status = "Failed"
+		else:
+			if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'):
+				doc.agreement_status = "First Response Due"
+			elif get_datetime(doc.get('first_responded_on')) <= get_datetime(doc.get('response_by')):
+				doc.agreement_status = "Fulfilled"
+			else:
+				doc.agreement_status = "Failed"
 
 
 def is_holiday(date, holidays):
@@ -835,23 +828,6 @@
 	return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
 
 
-def set_response_by_and_variance(doc, meta, start_date_time, priority):
-	if meta.has_field("response_by"):
-		doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time)
-
-	if meta.has_field("response_by_variance") and not doc.get('first_responded_on'):
-		now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
-		doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2)
-
-def set_resolution_by_and_variance(doc, meta, start_date_time, priority):
-	if meta.has_field("resolution_by"):
-		doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time)
-
-	if meta.has_field("resolution_by_variance") and not doc.get("resolution_date"):
-		now_time = frappe.flags.current_time or now_datetime(doc.get("owner"))
-		doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2)
-
-
 def now_datetime(user):
 	dt = convert_utc_to_user_timezone(datetime.utcnow(), user)
 	return dt.replace(tzinfo=None)
diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
index cfbe744..b07c862 100644
--- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
@@ -220,42 +220,6 @@
 		lead.reload()
 		self.assertEqual(lead.agreement_status, 'Fulfilled')
 
-	def test_changing_of_variance_after_response(self):
-		# create lead
-		doctype = "Lead"
-		lead_sla = create_service_level_agreement(
-			default_service_level_agreement=1,
-			holiday_list="__Test Holiday List",
-			entity_type=None, entity=None,
-			response_time=14400,
-			doctype=doctype,
-			sla_fulfilled_on=[{"status": "Replied"}],
-			apply_sla_for_resolution=0
-		)
-		creation = datetime.datetime(2019, 3, 4, 12, 0)
-		lead = make_lead(creation=creation, index=2)
-		self.assertEqual(lead.service_level_agreement, lead_sla.name)
-
-		# set lead as replied to set first responded on
-		frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 30)
-		lead.reload()
-		lead.status = 'Replied'
-		lead.save()
-		lead.reload()
-		self.assertEqual(lead.agreement_status, 'Fulfilled')
-
-		# check response_by_variance
-		self.assertEqual(lead.first_responded_on, frappe.flags.current_time)
-		self.assertEqual(lead.response_by_variance, 1800.0)
-
-		# make a change on the document &
-		# check response_by_variance is unchanged
-		frappe.flags.current_time = datetime.datetime(2019, 3, 4, 18, 30)
-		lead.status = 'Open'
-		lead.save()
-		lead.reload()
-		self.assertEqual(lead.response_by_variance, 1800.0)
-
 	def test_service_level_agreement_filters(self):
 		doctype = "Lead"
 		lead_sla = create_service_level_agreement(
@@ -295,7 +259,8 @@
 	return service_level_agreement
 
 def create_service_level_agreement(default_service_level_agreement, holiday_list, response_time, entity_type,
-	entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1):
+	entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1,
+	service_level=None, start_time="10:00:00", end_time="18:00:00"):
 
 	make_holiday_list()
 	make_priorities()
@@ -312,7 +277,7 @@
 		"doctype": "Service Level Agreement",
 		"enabled": 1,
 		"document_type": doctype,
-		"service_level": "__Test {} SLA".format(entity_type if entity_type else "Default"),
+		"service_level": service_level or "__Test {} SLA".format(entity_type if entity_type else "Default"),
 		"default_service_level_agreement": default_service_level_agreement,
 		"condition": condition,
 		"default_priority": "Medium",
@@ -345,28 +310,28 @@
 		"support_and_resolution": [
 			{
 				"workday": "Monday",
-				"start_time": "10:00:00",
-				"end_time": "18:00:00",
+				"start_time": start_time,
+				"end_time": end_time,
 			},
 			{
 				"workday": "Tuesday",
-				"start_time": "10:00:00",
-				"end_time": "18:00:00",
+				"start_time": start_time,
+				"end_time": end_time,
 			},
 			{
 				"workday": "Wednesday",
-				"start_time": "10:00:00",
-				"end_time": "18:00:00",
+				"start_time": start_time,
+				"end_time": end_time,
 			},
 			{
 				"workday": "Thursday",
-				"start_time": "10:00:00",
-				"end_time": "18:00:00",
+				"start_time": start_time,
+				"end_time": end_time,
 			},
 			{
 				"workday": "Friday",
-				"start_time": "10:00:00",
-				"end_time": "18:00:00",
+				"start_time": start_time,
+				"end_time": end_time,
 			}
 		]
 	})
@@ -386,7 +351,7 @@
 	if sla:
 		frappe.delete_doc("Service Level Agreement", sla, force=1)
 
-	return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True)
+	return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True, ignore_if_duplicate=True)
 
 
 def create_customer():
@@ -443,6 +408,13 @@
 	create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List",
 		entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800)
 
+	create_service_level_agreement(
+		default_service_level_agreement=0, holiday_list="__Test Holiday List",
+		entity_type=None, entity=None, response_time=14400, resolution_time=21600,
+		service_level="24-hour-SLA", start_time="00:00:00", end_time="23:59:59",
+		condition="doc.issue_type == 'Critical'"
+	)
+
 def make_holiday_list():
 	holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List")
 	if not holiday_list:
diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py
index 39a5c40..67fe345 100644
--- a/erpnext/support/report/issue_summary/issue_summary.py
+++ b/erpnext/support/report/issue_summary/issue_summary.py
@@ -82,7 +82,8 @@
 		self.sla_status_map = {
 			'SLA Failed': 'failed',
 			'SLA Fulfilled': 'fulfilled',
-			'SLA Ongoing': 'ongoing'
+			'First Response Due': 'first_response_due',
+			'Resolution Due': 'resolution_due'
 		}
 
 		for label, fieldname in self.sla_status_map.items():
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index ca03a78..d46ffb5 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -1847,7 +1847,7 @@
 Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1},
 Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:,
 Owner,Besitzer,
-PAN,PFANNE,
+PAN,PAN,
 POS,Verkaufsstelle,
 POS Profile,Verkaufsstellen-Profil,
 POS Profile is required to use Point-of-Sale,"POS-Profil ist erforderlich, um Point-of-Sale zu verwenden",
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 14b3afa..1d8b3a8 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -162,6 +162,28 @@
 
 		return ret
 
+	def reset_default_field_value(self, default_field: str, child_table: str, child_table_field: str):
+		""" Reset "Set default X" fields on forms to avoid confusion.
+
+			example:
+				doc = {
+					"set_from_warehouse": "Warehouse A",
+					"items": [{"from_warehouse": "warehouse B"}, {"from_warehouse": "warehouse A"}],
+				}
+				Since this has dissimilar values in child table, the default field will be erased.
+
+				doc.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
+			"""
+		child_table_values = set()
+
+		for row in self.get(child_table):
+			child_table_values.add(row.get(child_table_field))
+
+		if len(child_table_values) > 1:
+			self.set(default_field, None)
+		else:
+			self.set(default_field, list(child_table_values)[0])
+
 def delete_events(ref_type, ref_name):
 	events = frappe.db.sql_list(""" SELECT
 			distinct `tabEvent`.name