Merge branch 'develop' into deferred_report_revenue_and_expense
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/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..040624f 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,17 @@
   {
    "fieldname": "dimension_col_break",
    "fieldtype": "Column Break"
+  },
+  {
+   "description": "Reference number of the invoice from the previous system",
+   "fieldname": "invoice_number",
+   "fieldtype": "Data",
+   "label": "Invoice Number"
   }
  ],
  "istable": 1,
- "modified": "2019-07-25 15:00:00.460695",
+ "links": [],
+ "modified": "2021-12-13 18:15:41.295007",
  "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 f4fd1bf..df957d2 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -297,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/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/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/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/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 63530ea..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"
 		]
 	},
@@ -343,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/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/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/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_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..dac7b36 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -75,6 +75,15 @@
 			&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
 			frm.trigger("prepare_timer_buttons");
 		}
+
+		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_corrective_job_card: function(frm) {
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/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..165e0ac 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)
 
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 26fb859..65ad79e 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,6 +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
+erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py
index 0bb86e0..f4f9b17 100644
--- a/erpnext/patches/v13_0/rename_ksa_qr_field.py
+++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py
@@ -2,6 +2,7 @@
 # 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
 
 
@@ -12,5 +13,20 @@
 
 	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 cad1659..93a2731 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -835,7 +835,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: {
@@ -888,9 +888,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 = `
@@ -903,9 +905,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 += `
@@ -928,8 +932,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/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/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/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/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/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/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/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/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/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/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():