Merge branch 'develop' of https://github.com/frappe/erpnext into nil_exempt_non_gst_gstr_1
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 8f93811..4d61f1f 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -40,6 +40,7 @@
         - HR
         - projects
         - support
+        - CRM
         - assets
         - integrations
         - quality
@@ -48,6 +49,7 @@
         - agriculture
         - education
         - non-profit
+        - other
     validations:
       required: true
 
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 0000000..3aaba71
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,55 @@
+accounts:
+- erpnext/accounts/*
+- erpnext/controllers/accounts_controller.py
+- erpnext/controllers/taxes_and_totals.py
+
+stock:
+- erpnext/stock/*
+- erpnext/controllers/stock_controller.py
+- erpnext/controllers/item_variant.py
+
+assets:
+- erpnext/assets/*
+
+regional:
+- erpnext/regional/*
+
+selling:
+- erpnext/selling/*
+- erpnext/controllers/selling_controller.py
+
+buying:
+- erpnext/buying/*
+- erpnext/controllers/buying_controller.py
+
+support:
+- erpnext/support/*
+
+POS:
+- pos*
+
+ecommerce:
+- erpnext/e_commerce/*
+
+maintenance:
+- erpnext/maintenance/*
+
+manufacturing:
+- erpnext/manufacturing/*
+
+crm:
+- erpnext/crm/*
+
+HR:
+- erpnext/hr/*
+
+payroll:
+- erpnext/payroll*
+
+projects:
+- erpnext/projects/*
+
+# Any python files modifed but no test files modified
+needs-tests:
+- any: ['erpnext/**/*.py']
+  all: ['!erpnext/**/test*.py']
diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml
index db46c56..b644568 100644
--- a/.github/workflows/docs-checker.yml
+++ b/.github/workflows/docs-checker.yml
@@ -12,7 +12,7 @@
       - name: 'Setup Environment'
         uses: actions/setup-python@v2
         with:
-          python-version: 3.6
+          python-version: 3.8
 
       - name: 'Clone repo'
         uses: actions/checkout@v2
diff --git a/.github/workflows/labeller.yml b/.github/workflows/labeller.yml
new file mode 100644
index 0000000..a774400
--- /dev/null
+++ b/.github/workflows/labeller.yml
@@ -0,0 +1,12 @@
+name: "Pull Request Labeler"
+on:
+  pull_request_target:
+    types: [opened, reopened]
+
+jobs:
+  triage:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/labeler@v3
+      with:
+        repo-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index 33a28ac..d05bbbe 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -34,7 +34,7 @@
       - name: Setup Python
         uses: actions/setup-python@v2
         with:
-          python-version: 3.7
+          python-version: 3.8
 
       - name: Setup Node
         uses: actions/setup-node@v2
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 186f95e..7347a58 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -46,7 +46,7 @@
       - name: Setup Python
         uses: actions/setup-python@v2
         with:
-          python-version: 3.7
+          python-version: 3.8
 
       - name: Setup Node
         uses: actions/setup-node@v2
diff --git a/.github/workflows/server-tests-postgres.yml b/.github/workflows/server-tests-postgres.yml
index 3bbf6a9..77d3c1a 100644
--- a/.github/workflows/server-tests-postgres.yml
+++ b/.github/workflows/server-tests-postgres.yml
@@ -46,7 +46,7 @@
       - name: Setup Python
         uses: actions/setup-python@v2
         with:
-          python-version: 3.7
+          python-version: 3.8
 
       - name: Setup Node
         uses: actions/setup-node@v2
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index d765f04..ab6a53b 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -36,7 +36,7 @@
       - name: Setup Python
         uses: actions/setup-python@v2
         with:
-          python-version: 3.7
+          python-version: 3.8
 
       - uses: actions/setup-node@v2
         with:
diff --git a/CODEOWNERS b/CODEOWNERS
index a4a14de..bfc2601 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -23,13 +23,13 @@
 
 erpnext/crm/                    @ruchamahabal @pateljannat
 erpnext/education/              @ruchamahabal @pateljannat
-erpnext/healthcare/             @ruchamahabal @pateljannat @chillaranand
 erpnext/hr/                     @ruchamahabal @pateljannat
-erpnext/non_profit/             @ruchamahabal
 erpnext/payroll                 @ruchamahabal @pateljannat
 erpnext/projects/               @ruchamahabal @pateljannat
 
-erpnext/controllers             @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
+erpnext/controllers/            @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination @ankush
+erpnext/patches/                @deepeshgarg007 @nextchamp-saqib @marination @ankush
+erpnext/public/                 @nextchamp-saqib @marination
 
-.github/                        @surajshetty3416 @ankush
+.github/                        @ankush
 requirements.txt                @gavindsouza
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/__init__.py b/erpnext/accounts/doctype/currency_exchange_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/__init__.py
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js
new file mode 100644
index 0000000..6c40f2b
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js
@@ -0,0 +1,45 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Currency Exchange Settings', {
+	service_provider: function(frm) {
+		if (frm.doc.service_provider == "exchangerate.host") {
+			let result = ['result'];
+			let params = {
+				date: '{transaction_date}',
+				from: '{from_currency}',
+				to: '{to_currency}'
+			};
+			add_param(frm, "https://api.exchangerate.host/convert", params, result);
+		} else if (frm.doc.service_provider == "frankfurter.app") {
+			let result = ['rates', '{to_currency}'];
+			let params = {
+				base: '{from_currency}',
+				symbols: '{to_currency}'
+			};
+			add_param(frm, "https://frankfurter.app/{transaction_date}", params, result);
+		}
+	}
+});
+
+
+function add_param(frm, api, params, result) {
+	var row;
+	frm.clear_table("req_params");
+	frm.clear_table("result_key");
+
+	frm.doc.api_endpoint = api;
+
+	$.each(params, function(key, value) {
+		row = frm.add_child("req_params");
+		row.key = key;
+		row.value = value;
+	});
+
+	$.each(result, function(key, value) {
+		row = frm.add_child("result_key");
+		row.key = value;
+	});
+
+	frm.refresh_fields();
+}
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
new file mode 100644
index 0000000..7921fcc
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
@@ -0,0 +1,126 @@
+{
+ "actions": [],
+ "creation": "2022-01-10 13:03:26.237081",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "api_details_section",
+  "service_provider",
+  "api_endpoint",
+  "url",
+  "column_break_3",
+  "help",
+  "section_break_2",
+  "req_params",
+  "column_break_4",
+  "result_key"
+ ],
+ "fields": [
+  {
+   "fieldname": "api_details_section",
+   "fieldtype": "Section Break",
+   "label": "API Details"
+  },
+  {
+   "fieldname": "api_endpoint",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "API Endpoint",
+   "read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
+   "reqd": 1
+  },
+  {
+   "fieldname": "url",
+   "fieldtype": "Data",
+   "label": "Example URL",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_3",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "help",
+   "fieldtype": "HTML",
+   "label": "Help",
+   "options": "<h3>Currency Exchange Settings Help</h3>\n<p>There are 3 variables that could be used within the endpoint, result key and in values of the parameter.</p>\n<p>Exchange rate between {from_currency} and {to_currency} on {transaction_date} is fetched by the API.</p>\n<p>Example: If your endpoint is exchange.com/2021-08-01, then, you will have to input exchange.com/{transaction_date}</p>"
+  },
+  {
+   "fieldname": "section_break_2",
+   "fieldtype": "Section Break",
+   "label": "Request Parameters"
+  },
+  {
+   "fieldname": "req_params",
+   "fieldtype": "Table",
+   "label": "Parameters",
+   "options": "Currency Exchange Settings Details",
+   "read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
+   "reqd": 1
+  },
+  {
+   "fieldname": "column_break_4",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "result_key",
+   "fieldtype": "Table",
+   "label": "Result Key",
+   "options": "Currency Exchange Settings Result",
+   "read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
+   "reqd": 1
+  },
+  {
+   "fieldname": "service_provider",
+   "fieldtype": "Select",
+   "label": "Service Provider",
+   "options": "frankfurter.app\nexchangerate.host\nCustom",
+   "reqd": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2022-01-10 15:51:14.521174",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Currency Exchange Settings",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  },
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "Accounts Manager",
+   "share": 1,
+   "write": 1
+  },
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "Accounts User",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
new file mode 100644
index 0000000..e16ff3a
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
@@ -0,0 +1,82 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+import requests
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import nowdate
+
+
+class CurrencyExchangeSettings(Document):
+	def validate(self):
+		self.set_parameters_and_result()
+		response, value = self.validate_parameters()
+		self.validate_result(response, value)
+
+	def set_parameters_and_result(self):
+		if self.service_provider == 'exchangerate.host':
+			self.set('result_key', [])
+			self.set('req_params', [])
+
+			self.api_endpoint = "https://api.exchangerate.host/convert"
+			self.append('result_key', {'key': 'result'})
+			self.append('req_params', {'key': 'date', 'value': '{transaction_date}'})
+			self.append('req_params', {'key': 'from', 'value': '{from_currency}'})
+			self.append('req_params', {'key': 'to', 'value': '{to_currency}'})
+		elif self.service_provider == 'frankfurter.app':
+			self.set('result_key', [])
+			self.set('req_params', [])
+
+			self.api_endpoint = "https://frankfurter.app/{transaction_date}"
+			self.append('result_key', {'key': 'rates'})
+			self.append('result_key', {'key': '{to_currency}'})
+			self.append('req_params', {'key': 'base', 'value': '{from_currency}'})
+			self.append('req_params', {'key': 'symbols', 'value': '{to_currency}'})
+
+	def validate_parameters(self):
+		if frappe.flags.in_test:
+			return None, None
+
+		params = {}
+		for row in self.req_params:
+			params[row.key] = row.value.format(
+				transaction_date=nowdate(),
+				to_currency='INR',
+				from_currency='USD'
+			)
+
+		api_url = self.api_endpoint.format(
+			transaction_date=nowdate(),
+			to_currency='INR',
+			from_currency='USD'
+		)
+
+		try:
+			response = requests.get(api_url, params=params)
+		except requests.exceptions.RequestException as e:
+			frappe.throw("Error: " + str(e))
+
+		response.raise_for_status()
+		value = response.json()
+
+		return response, value
+
+	def validate_result(self, response, value):
+		if frappe.flags.in_test:
+			return
+
+		try:
+			for key in self.result_key:
+				value = value[str(key.key).format(
+					transaction_date=nowdate(),
+					to_currency='INR',
+					from_currency='USD'
+				)]
+		except Exception:
+			frappe.throw("Invalid result key. Response: " + response.text)
+		if not isinstance(value, (int, float)):
+			frappe.throw(_("Returned exchange rate is neither integer not float."))
+
+		self.url = response.url
+		frappe.msgprint("Exchange rate of USD to INR is " + str(value))
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/test_currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/test_currency_exchange_settings.py
new file mode 100644
index 0000000..2778729
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/test_currency_exchange_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+import unittest
+
+
+class TestCurrencyExchangeSettings(unittest.TestCase):
+	pass
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_details/__init__.py b/erpnext/accounts/doctype/currency_exchange_settings_details/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_details/__init__.py
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.json b/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.json
new file mode 100644
index 0000000..3093587
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.json
@@ -0,0 +1,39 @@
+{
+ "actions": [],
+ "creation": "2021-09-02 14:54:49.033512",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "key",
+  "value"
+ ],
+ "fields": [
+  {
+   "fieldname": "key",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Key",
+   "reqd": 1
+  },
+  {
+   "fieldname": "value",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Value",
+   "reqd": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-11-03 19:14:55.889037",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Currency Exchange Settings Details",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.py b/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.py
new file mode 100644
index 0000000..a6ad763
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CurrencyExchangeSettingsDetails(Document):
+	pass
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_result/__init__.py b/erpnext/accounts/doctype/currency_exchange_settings_result/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_result/__init__.py
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.json b/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.json
new file mode 100644
index 0000000..fff5337
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.json
@@ -0,0 +1,31 @@
+{
+ "actions": [],
+ "creation": "2021-09-03 13:17:22.088259",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "key"
+ ],
+ "fields": [
+  {
+   "fieldname": "key",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Key",
+   "reqd": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-11-03 19:14:40.054245",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Currency Exchange Settings Result",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.py b/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.py
new file mode 100644
index 0000000..1774128
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CurrencyExchangeSettingsResult(Document):
+	pass
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index c1b056b..0e07abd 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1708,7 +1708,10 @@
 
 def apply_early_payment_discount(paid_amount, received_amount, doc):
 	total_discount = 0
-	if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
+	eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice']
+	has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule
+
+	if doc.doctype in eligible_for_payments and has_payment_schedule:
 		for term in doc.payment_schedule:
 			if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
 				if term.discount_type == 'Percentage':
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 11d59bc..134bccf 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -353,7 +353,6 @@
 			if not for_validate and not self.customer:
 				self.customer = profile.customer
 
-			self.ignore_pricing_rule = profile.ignore_pricing_rule
 			self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
 			self.set_warehouse = profile.get('warehouse') or self.set_warehouse
 
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 7d31e0a..56479a0 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -556,6 +556,37 @@
 		batch.cancel()
 		batch.delete()
 
+	def test_ignore_pricing_rule(self):
+		from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule
+
+		item_price = frappe.get_doc({
+			'doctype': 'Item Price',
+			'item_code': '_Test Item',
+			'price_list': '_Test Price List',
+			'price_list_rate': '450',
+		})
+		item_price.insert()
+		pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
+		pr.save()
+		pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
+		pos_inv.items[0].rate = 300
+		pos_inv.save()
+		self.assertEquals(pos_inv.items[0].discount_percentage, 10)
+		# rate shouldn't change
+		self.assertEquals(pos_inv.items[0].rate, 405)
+
+		pos_inv.ignore_pricing_rule = 1
+		pos_inv.items[0].rate = 300
+		pos_inv.save()
+		self.assertEquals(pos_inv.ignore_pricing_rule, 1)
+		# rate should change since pricing rules are ignored
+		self.assertEquals(pos_inv.items[0].rate, 300)
+
+		item_price.delete()
+		pos_inv.delete()
+		pr.delete()
+
+
 def create_pos_invoice(**args):
 	args = frappe._dict(args)
 	pos_profile = None
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index 314c894..5746a84 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -650,7 +650,7 @@
 		"rate": args.rate or 0.0,
 		"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
 		"condition": args.condition or '',
-		"priority": 1,
+		"priority": args.priority or 1,
 		"discount_amount": args.discount_amount or 0.0,
 		"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
 	})
@@ -676,6 +676,8 @@
 	if args.get(applicable_for):
 		doc.db_set(applicable_for, args.get(applicable_for))
 
+	return doc
+
 def setup_pricing_rule_data():
 	if not frappe.db.exists('Campaign', '_Test Campaign'):
 		frappe.get_doc({
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 545abf7..5062c1c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -651,7 +651,7 @@
    "hide_seconds": 1,
    "label": "Ignore Pricing Rule",
    "no_copy": 1,
-   "permlevel": 1,
+   "permlevel": 0,
    "print_hide": 1
   },
   {
@@ -2038,7 +2038,7 @@
    "link_fieldname": "consolidated_invoice"
   }
  ],
- "modified": "2021-10-21 20:19:38.667508",
+ "modified": "2021-12-23 20:19:38.667508",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Sales Invoice",
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
index a4842c1..3a51db8 100644
--- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
@@ -121,20 +121,21 @@
 		"""
 		simulate future posting by creating dummy gl entries. starts from the last posting date.
 		"""
-		if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
-			self.estimate_for_period_list = get_period_list(
-				self.filters.from_fiscal_year,
-				self.filters.to_fiscal_year,
-				add_days(self.last_entry_date, 1),
-				self.period_list[-1].to_date,
-				"Date Range",
-				"Monthly",
-				company=self.filters.company,
-			)
-			for period in self.estimate_for_period_list:
-				amount = self.calculate_amount(period.from_date, period.to_date)
-				gle = self.make_dummy_gle(period.key, period.to_date, amount)
-				self.gle_entries.append(gle)
+		if self.service_start_date != self.service_end_date:
+			if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
+				self.estimate_for_period_list = get_period_list(
+					self.filters.from_fiscal_year,
+					self.filters.to_fiscal_year,
+					add_days(self.last_entry_date, 1),
+					self.period_list[-1].to_date,
+					"Date Range",
+					"Monthly",
+					company=self.filters.company,
+				)
+				for period in self.estimate_for_period_list:
+					amount = self.calculate_amount(period.from_date, period.to_date)
+					gle = self.make_dummy_gle(period.key, period.to_date, amount)
+					self.gle_entries.append(gle)
 
 	def calculate_item_revenue_expense_for_period(self):
 		"""
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index b296876..010284c 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -167,7 +167,7 @@
 			"fieldname": "include_dimensions",
 			"label": __("Consider Accounting Dimensions"),
 			"fieldtype": "Check",
-			"default": 0
+			"default": 1
 		},
 		{
 			"fieldname": "show_opening_entries",
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 385c8b2..7f27920 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -448,9 +448,11 @@
 
 			elif group_by_voucher_consolidated:
 				keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
-				for dim in accounting_dimensions:
-					keylist.append(gle.get(dim))
-				keylist.append(gle.get("cost_center"))
+				if filters.get("include_dimensions"):
+					for dim in accounting_dimensions:
+						keylist.append(gle.get(dim))
+					keylist.append(gle.get("cost_center"))
+
 				key = tuple(keylist)
 				if key not in consolidated_gle:
 					consolidated_gle.setdefault(key, gle)
@@ -547,10 +549,7 @@
 			"fieldname": "balance",
 			"fieldtype": "Float",
 			"width": 130
-		}
-	]
-
-	columns.extend([
+		},
 		{
 			"label": _("Voucher Type"),
 			"fieldname": "voucher_type",
@@ -584,7 +583,7 @@
 			"fieldname": "project",
 			"width": 100
 		}
-	])
+	]
 
 	if filters.get("include_dimensions"):
 		for dim in get_accounting_dimensions(as_list = False):
@@ -594,14 +593,14 @@
 				"fieldname": dim.fieldname,
 				"width": 100
 			})
-
-	columns.extend([
-		{
+		columns.append({
 			"label": _("Cost Center"),
 			"options": "Cost Center",
 			"fieldname": "cost_center",
 			"width": 100
-		},
+		})
+
+	columns.extend([
 		{
 			"label": _("Against Voucher Type"),
 			"fieldname": "against_voucher_type",
diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py
new file mode 100644
index 0000000..78c109a
--- /dev/null
+++ b/erpnext/accounts/test/test_reports.py
@@ -0,0 +1,48 @@
+import unittest
+from typing import List, Tuple
+
+from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
+
+DEFAULT_FILTERS = {
+	"company": "_Test Company",
+	"from_date": "2010-01-01",
+	"to_date": "2030-01-01",
+	"period_start_date": "2010-01-01",
+	"period_end_date": "2030-01-01"
+}
+
+
+REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
+	("General Ledger", {"group_by": "Group by Voucher (Consolidated)"} ),
+	("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1} ),
+	("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
+	("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
+	("Consolidated Financial Statement", {"report": "Balance Sheet"} ),
+	("Consolidated Financial Statement", {"report": "Profit and Loss Statement"} ),
+	("Consolidated Financial Statement", {"report": "Cash Flow"} ),
+	("Gross Profit", {"group_by": "Invoice"}),
+	("Gross Profit", {"group_by": "Item Code"}),
+	("Gross Profit", {"group_by": "Item Group"}),
+	("Gross Profit", {"group_by": "Customer"}),
+	("Gross Profit", {"group_by": "Customer Group"}),
+	("Item-wise Sales Register", {}),
+	("Item-wise Purchase Register", {}),
+	("Sales Register", {}),
+	("Purchase Register", {}),
+	("Tax Detail", {"mode": "run", "report_name": "Tax Detail"},),
+]
+
+OPTIONAL_FILTERS = {}
+
+
+class TestReports(unittest.TestCase):
+	def test_execute_all_accounts_reports(self):
+		"""Test that all script report in stock modules are executable with supported filters"""
+		for report, filter in REPORT_FILTER_TEST_CASES:
+			execute_script_report(
+				report_name=report,
+				module="Accounts",
+				filters=filter,
+				default_filters=DEFAULT_FILTERS,
+				optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+			)
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index a18b03a..ee3ec8e 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -608,7 +608,17 @@
 		return purchase_document
 
 	def get_fixed_asset_account(self):
-		return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
+		fixed_asset_account = get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
+		if not fixed_asset_account:
+			frappe.throw(
+				_("Set {0} in asset category {1} for company {2}").format(
+					frappe.bold("Fixed Asset Account"),
+					frappe.bold(self.asset_category),
+					frappe.bold(self.company),
+				),
+				title=_("Account not Found"),
+			)
+		return fixed_asset_account
 
 	def get_cwip_account(self, cwip_enabled=False):
 		cwip_account = None
diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py
index 05541d1..908d78c 100644
--- a/erpnext/controllers/tests/test_queries.py
+++ b/erpnext/controllers/tests/test_queries.py
@@ -1,6 +1,8 @@
 import unittest
 from functools import partial
 
+import frappe
+
 from erpnext.controllers import queries
 
 
@@ -85,3 +87,6 @@
 
 		wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
 		self.assertGreaterEqual(len(wh), 1)
+
+	def test_default_uoms(self):
+		self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10)
diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py
index 13aa697..f4d3f97 100644
--- a/erpnext/controllers/tests/test_transaction_base.py
+++ b/erpnext/controllers/tests/test_transaction_base.py
@@ -4,19 +4,72 @@
 
 
 class TestUtils(unittest.TestCase):
-    def test_reset_default_field_value(self):
-        doc = frappe.get_doc({
-            "doctype": "Purchase Receipt",
-            "set_warehouse": "Warehouse 1",
-        })
+	def test_reset_default_field_value(self):
+		doc = frappe.get_doc({
+			"doctype": "Purchase Receipt",
+			"set_warehouse": "Warehouse 1",
+		})
 
-        # Same values
-        doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
-        doc.reset_default_field_value("set_warehouse", "items", "warehouse")
-        self.assertEqual(doc.set_warehouse, "Warehouse 1")
+		# Same values
+		doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
+		doc.reset_default_field_value("set_warehouse", "items", "warehouse")
+		self.assertEqual(doc.set_warehouse, "Warehouse 1")
 
-        # Mixed values
-        doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
-        doc.reset_default_field_value("set_warehouse", "items", "warehouse")
-        self.assertEqual(doc.set_warehouse, None)
+		# Mixed values
+		doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
+		doc.reset_default_field_value("set_warehouse", "items", "warehouse")
+		self.assertEqual(doc.set_warehouse, None)
 
+	def test_reset_default_field_value_in_mfg_stock_entry(self):
+		# manufacture stock entry with rows having blank source/target wh
+		se = frappe.get_doc(
+			doctype="Stock Entry",
+			purpose="Manufacture",
+			stock_entry_type="Manufacture",
+			company="_Test Company",
+			from_warehouse="_Test Warehouse - _TC",
+			to_warehouse="_Test Warehouse 1 - _TC",
+			items=[
+				frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"),
+				frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1)
+			]
+		)
+		se.save()
+
+		# default fields must be untouched
+		self.assertEqual(se.from_warehouse, "_Test Warehouse - _TC")
+		self.assertEqual(se.to_warehouse, "_Test Warehouse 1 - _TC")
+
+		se.delete()
+
+	def test_reset_default_field_value_in_transfer_stock_entry(self):
+		doc = frappe.get_doc({
+			"doctype": "Stock Entry",
+			"purpose": "Material Receipt",
+			"from_warehouse": "Warehouse 1",
+			"to_warehouse": "Warehouse 2",
+		})
+
+		# Same values
+		doc.items = [
+			{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
+			{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
+			{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
+		]
+
+		doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
+		doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
+		self.assertEqual(doc.from_warehouse, "Warehouse 1")
+		self.assertEqual(doc.to_warehouse, "Warehouse 2")
+
+		# Mixed values in source wh
+		doc.items = [
+			{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
+			{"s_warehouse": "Warehouse 3", "t_warehouse": "Warehouse 2"},
+			{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
+		]
+
+		doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
+		doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
+		self.assertEqual(doc.from_warehouse, None)
+		self.assertEqual(doc.to_warehouse, "Warehouse 2")
\ No newline at end of file
diff --git a/erpnext/education/api.py b/erpnext/education/api.py
index d9013b0..636b948 100644
--- a/erpnext/education/api.py
+++ b/erpnext/education/api.py
@@ -201,8 +201,8 @@
 	conditions = get_event_conditions("Course Schedule", filters)
 
 	data = frappe.db.sql("""select name, course, color,
-			timestamp(schedule_date, from_time) as from_datetime,
-			timestamp(schedule_date, to_time) as to_datetime,
+			timestamp(schedule_date, from_time) as from_time,
+			timestamp(schedule_date, to_time) as to_time,
 			room, student_group, 0 as 'allDay'
 		from `tabCourse Schedule`
 		where ( schedule_date between %(start)s and %(end)s )
diff --git a/erpnext/education/doctype/course_schedule/course_schedule.py b/erpnext/education/doctype/course_schedule/course_schedule.py
index ffd323d..615d2c4 100644
--- a/erpnext/education/doctype/course_schedule/course_schedule.py
+++ b/erpnext/education/doctype/course_schedule/course_schedule.py
@@ -3,6 +3,8 @@
 # For license information, please see license.txt
 
 
+from datetime import datetime
+
 import frappe
 from frappe import _
 from frappe.model.document import Document
@@ -30,6 +32,14 @@
 		if self.from_time > self.to_time:
 			frappe.throw(_("From Time cannot be greater than To Time."))
 
+		"""Handles specicfic case to update schedule date in calendar """
+		if isinstance(self.from_time, str):
+			try:
+				datetime_obj = datetime.strptime(self.from_time, '%Y-%m-%d %H:%M:%S')
+				self.schedule_date = datetime_obj
+			except ValueError:
+				pass
+
 	def validate_overlap(self):
 		"""Validates overlap for Student Group, Instructor, Room"""
 
@@ -47,4 +57,4 @@
 			validate_overlap_for(self, "Assessment Plan", "student_group")
 
 		validate_overlap_for(self, "Assessment Plan", "room")
-		validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)
+		validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)
\ No newline at end of file
diff --git a/erpnext/education/doctype/course_schedule/course_schedule_calendar.js b/erpnext/education/doctype/course_schedule/course_schedule_calendar.js
index 803527e..cacd539 100644
--- a/erpnext/education/doctype/course_schedule/course_schedule_calendar.js
+++ b/erpnext/education/doctype/course_schedule/course_schedule_calendar.js
@@ -1,11 +1,10 @@
 frappe.views.calendar["Course Schedule"] = {
 	field_map: {
-		// from_datetime and to_datetime don't exist as docfields but are used in onload
-		"start": "from_datetime",
-		"end": "to_datetime",
+		"start": "from_time",
+		"end": "to_time",
 		"id": "name",
 		"title": "course",
-		"allDay": "allDay"
+		"allDay": "allDay",
 	},
 	gantt: false,
 	order_by: "schedule_date",
diff --git a/erpnext/education/doctype/course_schedule/test_course_schedule.py b/erpnext/education/doctype/course_schedule/test_course_schedule.py
index a732419..56149af 100644
--- a/erpnext/education/doctype/course_schedule/test_course_schedule.py
+++ b/erpnext/education/doctype/course_schedule/test_course_schedule.py
@@ -6,6 +6,7 @@
 
 import frappe
 from frappe.utils import to_timedelta, today
+from frappe.utils.data import add_to_date
 
 from erpnext.education.utils import OverlapError
 
@@ -39,6 +40,11 @@
 		make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time,
 			student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name)
 
+	def test_update_schedule_date(self):
+		doc = make_course_schedule_test_record(schedule_date= add_to_date(today(), days=1))
+		doc.schedule_date = add_to_date(doc.schedule_date, days=1)
+		doc.save()
+
 def make_course_schedule_test_record(**args):
 	args = frappe._dict(args)
 
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index cb1b560..2d129c8 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -19,7 +19,7 @@
 		if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
 			frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
 
-		project = "Employee Onboarding : Test Researcher - test@researcher.com"
+		project = "Employee Onboarding : test@researcher.com"
 		frappe.db.sql("delete from tabProject where name=%s", project)
 		frappe.db.sql("delete from tabTask where project=%s", project)
 
@@ -27,7 +27,7 @@
 		onboarding = create_employee_onboarding()
 
 		project_name = frappe.db.get_value('Project', onboarding.project, 'project_name')
-		self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
+		self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com')
 
 		# don't allow making employee if onboarding is not complete
 		self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
@@ -64,8 +64,8 @@
 
 
 def get_job_applicant():
-	if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
-		return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
+	if frappe.db.exists('Job Applicant', 'test@researcher.com'):
+		return frappe.get_doc('Job Applicant', 'test@researcher.com')
 	applicant = frappe.new_doc('Job Applicant')
 	applicant.applicant_name = 'Test Researcher'
 	applicant.email_id = 'test@researcher.com'
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json
index 200f675..66b609c 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.json
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.json
@@ -192,10 +192,11 @@
  "idx": 1,
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2021-09-29 23:06:10.904260",
+ "modified": "2022-01-12 16:28:53.196881",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "Job Applicant",
+ "naming_rule": "Expression (old style)",
  "owner": "Administrator",
  "permissions": [
   {
@@ -210,10 +211,11 @@
    "write": 1
   }
  ],
- "search_fields": "applicant_name",
+ "search_fields": "applicant_name, email_id, job_title, phone_number",
  "sender_field": "email_id",
  "sort_field": "modified",
  "sort_order": "ASC",
+ "states": [],
  "subject_field": "notes",
  "title_field": "applicant_name"
 }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py
index abaa50c..5b3d9bf 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.py
@@ -7,6 +7,7 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
+from frappe.model.naming import append_number_if_name_exists
 from frappe.utils import validate_email_address
 
 from erpnext.hr.doctype.interview.interview import get_interviewers
@@ -21,10 +22,11 @@
 			self.get("__onload").job_offer = job_offer[0].name
 
 	def autoname(self):
-		keys = filter(None, (self.applicant_name, self.email_id, self.job_title))
-		if not keys:
-			frappe.throw(_("Name or Email is mandatory"), frappe.NameError)
-		self.name = " - ".join(keys)
+		self.name = self.email_id
+
+		# applicant can apply more than once for a different job title or reapply
+		if frappe.db.exists("Job Applicant", self.name):
+			self.name = append_number_if_name_exists("Job Applicant", self.name)
 
 	def validate(self):
 		if self.email_id:
diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
index 36dcf6b..bf16220 100644
--- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
@@ -9,7 +9,26 @@
 
 
 class TestJobApplicant(unittest.TestCase):
-	pass
+	def test_job_applicant_naming(self):
+		applicant = frappe.get_doc({
+			"doctype": "Job Applicant",
+			"status": "Open",
+			"applicant_name": "_Test Applicant",
+			"email_id": "job_applicant_naming@example.com"
+		}).insert()
+		self.assertEqual(applicant.name, 'job_applicant_naming@example.com')
+
+		applicant = frappe.get_doc({
+			"doctype": "Job Applicant",
+			"status": "Open",
+			"applicant_name": "_Test Applicant",
+			"email_id": "job_applicant_naming@example.com"
+		}).insert()
+		self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1')
+
+	def tearDown(self):
+		frappe.db.rollback()
+
 
 def create_job_applicant(**args):
 	args = frappe._dict(args)
diff --git a/erpnext/hr/doctype/leave_period/leave_period.json b/erpnext/hr/doctype/leave_period/leave_period.json
index 9e895c3..84ce114 100644
--- a/erpnext/hr/doctype/leave_period/leave_period.json
+++ b/erpnext/hr/doctype/leave_period/leave_period.json
@@ -1,294 +1,108 @@
 {
- "allow_copy": 0,
- "allow_guest_to_view": 0,
+ "actions": [],
  "allow_import": 1,
  "allow_rename": 1,
  "autoname": "HR-LPR-.YYYY.-.#####",
- "beta": 0,
  "creation": "2018-04-13 15:20:52.864288",
- "custom": 0,
- "docstatus": 0,
  "doctype": "DocType",
- "document_type": "",
  "editable_grid": 1,
  "engine": "InnoDB",
+ "field_order": [
+  "from_date",
+  "to_date",
+  "is_active",
+  "column_break_3",
+  "company",
+  "optional_holiday_list"
+ ],
  "fields": [
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "from_date",
    "fieldtype": "Date",
-   "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": "From Date",
-   "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": 1,
-   "search_index": 0,
-   "set_only_once": 0,
-   "translatable": 0,
-   "unique": 0
+   "reqd": 1
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "to_date",
    "fieldtype": "Date",
-   "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": "To Date",
-   "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": 1,
-   "search_index": 0,
-   "set_only_once": 0,
-   "translatable": 0,
-   "unique": 0
+   "reqd": 1
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
+   "default": "0",
    "fieldname": "is_active",
    "fieldtype": "Check",
-   "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": "Is Active",
-   "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,
-   "translatable": 0,
-   "unique": 0
+   "label": "Is Active"
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "column_break_3",
-   "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,
-   "translatable": 0,
-   "unique": 0
+   "fieldtype": "Column Break"
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "company",
    "fieldtype": "Link",
-   "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": "Company",
-   "length": 0,
-   "no_copy": 0,
    "options": "Company",
-   "permlevel": 0,
-   "precision": "",
-   "print_hide": 0,
-   "print_hide_if_no_value": 0,
-   "read_only": 0,
-   "remember_last_selected_value": 0,
-   "report_hide": 0,
-   "reqd": 1,
-   "search_index": 0,
-   "set_only_once": 0,
-   "translatable": 0,
-   "unique": 0
+   "reqd": 1
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "optional_holiday_list",
    "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": "Holiday List for Optional Leave",
-   "length": 0,
-   "no_copy": 0,
-   "options": "Holiday List",
-   "permlevel": 0,
-   "precision": "",
-   "print_hide": 0,
-   "print_hide_if_no_value": 0,
-   "read_only": 0,
-   "remember_last_selected_value": 0,
-   "report_hide": 0,
-   "reqd": 0,
-   "search_index": 0,
-   "set_only_once": 0,
-   "translatable": 0,
-   "unique": 0
+   "options": "Holiday List"
   }
  ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-05-30 16:15:43.305502",
+ "links": [],
+ "modified": "2022-01-13 13:28:12.951025",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "Leave Period",
- "name_case": "",
+ "naming_rule": "Expression (old style)",
  "owner": "Administrator",
  "permissions": [
   {
-   "amend": 0,
-   "cancel": 0,
    "create": 1,
    "delete": 1,
    "email": 1,
    "export": 1,
-   "if_owner": 0,
-   "import": 0,
-   "permlevel": 0,
    "print": 1,
    "read": 1,
    "report": 1,
    "role": "System Manager",
-   "set_user_permissions": 0,
    "share": 1,
-   "submit": 0,
    "write": 1
   },
   {
-   "amend": 0,
-   "cancel": 0,
    "create": 1,
    "delete": 1,
    "email": 1,
    "export": 1,
-   "if_owner": 0,
-   "import": 0,
-   "permlevel": 0,
    "print": 1,
    "read": 1,
    "report": 1,
    "role": "HR Manager",
-   "set_user_permissions": 0,
    "share": 1,
-   "submit": 0,
    "write": 1
   },
   {
-   "amend": 0,
-   "cancel": 0,
    "create": 1,
    "delete": 1,
    "email": 1,
    "export": 1,
-   "if_owner": 0,
-   "import": 0,
-   "permlevel": 0,
    "print": 1,
    "read": 1,
    "report": 1,
    "role": "HR User",
-   "set_user_permissions": 0,
    "share": 1,
-   "submit": 0,
    "write": 1
   }
  ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
+ "search_fields": "from_date, to_date, company",
  "sort_field": "modified",
  "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "states": [],
+ "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
index 3373350..27f0540 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
@@ -113,10 +113,11 @@
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2021-03-01 17:54:01.014509",
+ "modified": "2022-01-13 13:37:11.218882",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "Leave Policy Assignment",
+ "naming_rule": "Expression (old style)",
  "owner": "Administrator",
  "permissions": [
   {
@@ -164,5 +165,7 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
+ "title_field": "employee_name",
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
index 8b954c4..6b75817 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
@@ -48,7 +48,16 @@
 						if (cur_dialog.fields_dict.leave_period.value) {
 							me.set_effective_date();
 						}
-					}
+					},
+					get_query() {
+						let filters = {"is_active": 1};
+						if (cur_dialog.fields_dict.company.value)
+							filters["company"] = cur_dialog.fields_dict.company.value;
+
+						return {
+							filters: filters
+						};
+					},
 				},
 				{
 					fieldtype: "Column Break"
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index f82d9a0..5a60fb7 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -530,16 +530,6 @@
 				row.hour_rate = (hour_rate / flt(self.conversion_rate)
 					if self.conversion_rate and hour_rate else hour_rate)
 
-			if self.routing:
-				time_in_mins = flt(frappe.db.get_value("BOM Operation", {
-						"workstation": row.workstation,
-						"operation": row.operation,
-						"parent": self.routing
-				}, ["time_in_mins"]))
-
-				if time_in_mins:
-					row.time_in_mins = time_in_mins
-
 		if row.hour_rate and row.time_in_mins:
 			row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
 			row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py
index e90b0a7..8bd60ea 100644
--- a/erpnext/manufacturing/doctype/routing/test_routing.py
+++ b/erpnext/manufacturing/doctype/routing/test_routing.py
@@ -46,6 +46,7 @@
 		wo_doc.delete()
 
 	def test_update_bom_operation_time(self):
+		"""Update cost shouldn't update routing times."""
 		operations = [
 			{
 				"operation": "Test Operation A",
@@ -85,8 +86,8 @@
 		routing_doc.save()
 		bom_doc.update_cost()
 		bom_doc.reload()
-		self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
-		self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
+		self.assertEqual(bom_doc.operations[0].time_in_mins, 30)
+		self.assertEqual(bom_doc.operations[1].time_in_mins, 20)
 
 
 def setup_operations(rows):
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 9926b15..e7eb9c6 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -2,7 +2,7 @@
 # License: GNU General Public License v3. See license.txt
 
 import frappe
-from frappe.utils import add_months, cint, flt, now, today
+from frappe.utils import add_days, add_months, cint, flt, now, today
 
 from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
 from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
@@ -12,6 +12,7 @@
 	OverProductionError,
 	StockOverProductionError,
 	close_work_order,
+	make_job_card,
 	make_stock_entry,
 	stop_unstop,
 )
@@ -804,6 +805,34 @@
 			if row.is_scrap_item:
 				self.assertEqual(row.qty, 1)
 
+		# Partial Job Card 1 with qty 10
+		wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=add_days(now(), 60), qty=20, skip_transfer=1)
+		job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
+		update_job_card(job_card, 10)
+
+		stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
+		for row in stock_entry.items:
+			if row.is_scrap_item:
+				self.assertEqual(row.qty, 2)
+
+		# Partial Job Card 2 with qty 10
+		operations = []
+		wo_order.load_from_db()
+		for row in wo_order.operations:
+			n_dict = row.as_dict()
+			n_dict['qty'] = 10
+			n_dict['pending_qty'] = 10
+			operations.append(n_dict)
+
+		make_job_card(wo_order.name, operations)
+		job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name, 'docstatus': 0}, 'name')
+		update_job_card(job_card, 10)
+
+		stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
+		for row in stock_entry.items:
+			if row.is_scrap_item:
+				self.assertEqual(row.qty, 2)
+
 	def test_close_work_order(self):
 		items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO',
 			'Test RM Item 2 for Closed WO']
@@ -883,7 +912,8 @@
 		self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins)
 
 
-def update_job_card(job_card):
+def update_job_card(job_card, jc_qty=None):
+	employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name')
 	job_card_doc = frappe.get_doc('Job Card', job_card)
 	job_card_doc.set('scrap_items', [
 		{
@@ -896,8 +926,12 @@
 		},
 	])
 
+	if jc_qty:
+		job_card_doc.for_quantity = jc_qty
+
 	job_card_doc.append('time_logs', {
 		'from_time': now(),
+		'employee': employee,
 		'time_in_mins': 60,
 		'completed_qty': job_card_doc.for_quantity
 	})
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 99741eb..5190f9f 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -279,6 +279,7 @@
 erpnext.patches.v13_0.update_recipient_email_digest
 erpnext.patches.v13_0.shopify_deprecation_warning
 erpnext.patches.v13_0.remove_bad_selling_defaults
+erpnext.patches.v13_0.trim_whitespace_from_serial_nos
 erpnext.patches.v13_0.migrate_stripe_api
 erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
 erpnext.patches.v13_0.einvoicing_deprecation_warning
@@ -304,6 +305,7 @@
 erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
 erpnext.patches.v13_0.requeue_failed_reposts
 erpnext.patches.v13_0.update_job_card_status
+erpnext.patches.v13_0.enable_uoms
 erpnext.patches.v12_0.update_production_plan_status
 erpnext.patches.v13_0.healthcare_deprecation_warning
 erpnext.patches.v13_0.item_naming_series_not_mandatory
@@ -322,3 +324,4 @@
 erpnext.patches.v14_0.set_payroll_cost_centers
 erpnext.patches.v13_0.agriculture_deprecation_warning
 erpnext.patches.v14_0.delete_agriculture_doctypes
+erpnext.patches.v13_0.update_exchange_rate_settings
diff --git a/erpnext/patches/v13_0/enable_uoms.py b/erpnext/patches/v13_0/enable_uoms.py
new file mode 100644
index 0000000..4d3f637
--- /dev/null
+++ b/erpnext/patches/v13_0/enable_uoms.py
@@ -0,0 +1,13 @@
+import frappe
+
+
+def execute():
+	frappe.reload_doc('setup', 'doctype', 'uom')
+
+	uom = frappe.qb.DocType("UOM")
+
+	(frappe.qb
+		.update(uom)
+		.set(uom.enabled, 1)
+		.where(uom.creation >= "2021-10-18")  # date when this field was released
+	).run()
diff --git a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
new file mode 100644
index 0000000..8a9633d
--- /dev/null
+++ b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
@@ -0,0 +1,65 @@
+import frappe
+
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+
+def execute():
+	broken_sles = frappe.db.sql("""
+			select name, serial_no
+			from `tabStock Ledger Entry`
+			where
+				is_cancelled = 0
+				and (serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s)
+			""",
+			(
+				" %",    # leading whitespace
+				"% ",    # trailing whitespace
+				"%\n %", # leading whitespace on newline
+				"% \n%", # trailing whitespace on newline
+			),
+			as_dict=True,
+		)
+
+	frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sles)
+
+	if not broken_sles:
+		return
+
+	broken_serial_nos = set()
+
+	# patch SLEs
+	for sle in broken_sles:
+		serial_no_list = get_serial_nos(sle.serial_no)
+		correct_sr_no = "\n".join(serial_no_list)
+
+		if correct_sr_no == sle.serial_no:
+			continue
+
+		frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False)
+		broken_serial_nos.update(serial_no_list)
+
+	if not broken_serial_nos:
+		return
+
+	# Patch serial No documents if they don't have purchase info
+	# Purchase info is used for fetching incoming rate
+	broken_sr_no_records = frappe.get_list("Serial No",
+			filters={
+				"status":"Active",
+				"name": ("in", broken_serial_nos),
+				"purchase_document_type": ("is", "not set")
+			},
+			pluck="name",
+		)
+
+	frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sr_no_records)
+
+	patch_savepoint = "serial_no_patch"
+	for serial_no in broken_sr_no_records:
+		try:
+			frappe.db.savepoint(patch_savepoint)
+			sn = frappe.get_doc("Serial No", serial_no)
+			sn.update_serial_no_reference()
+			sn.db_update()
+		except Exception:
+			frappe.db.rollback(save_point=patch_savepoint)
diff --git a/erpnext/patches/v13_0/update_exchange_rate_settings.py b/erpnext/patches/v13_0/update_exchange_rate_settings.py
new file mode 100644
index 0000000..b7ec232
--- /dev/null
+++ b/erpnext/patches/v13_0/update_exchange_rate_settings.py
@@ -0,0 +1,10 @@
+import frappe
+
+from erpnext.setup.install import setup_currency_exchange
+
+
+def execute():
+	frappe.reload_doc("accounts", "doctype", "currency_exchange_settings")
+	frappe.reload_doc("accounts", "doctype", "currency_exchange_settings_result")
+	frappe.reload_doc("accounts", "doctype", "currency_exchange_settings_details")
+	setup_currency_exchange()
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index ed3fa5b..db88c06 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -61,6 +61,8 @@
 	def on_cancel(self):
 		frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
 			where payroll_entry=%s """, (self.name)))
+		self.db_set("salary_slips_created", 0)
+		self.db_set("salary_slips_submitted", 0)
 
 	def get_emp_list(self):
 		"""
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 9b1ea04..8fa0538 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -102,7 +102,7 @@
 			frappe.throw(_("Completed On cannot be greater than Today"))
 
 	def update_depends_on(self):
-		depends_on_tasks = self.depends_on_tasks or ""
+		depends_on_tasks = ""
 		for d in self.depends_on:
 			if d.task and d.task not in depends_on_tasks:
 				depends_on_tasks += d.task + ","
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index 0c0acc7..b2bf546 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -68,7 +68,8 @@
 			(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
 			(soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount,
 			soi.warehouse as warehouse,
-			so.company, soi.name
+			so.company, soi.name,
+			soi.description as description
 		FROM
 			`tabSales Order` so,
 			(`tabSales Order Item` soi
@@ -184,6 +185,12 @@
 			"options": "Item",
 			"width": 100
 		})
+		columns.append({
+			"label":_("Description"),
+			"fieldname": "description",
+			"fieldtype": "Small Text",
+			"width": 100
+		})
 
 	columns.extend([
 		{
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 45e8dcc..dd185fc 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -213,6 +213,9 @@
 		["default_payroll_payable_account", {"root_type": "Liability"}],
 		["round_off_account", {"root_type": "Expense"}],
 		["write_off_account", {"root_type": "Expense"}],
+		["default_deferred_expense_account", {}],
+		["default_deferred_revenue_account", {}],
+		["default_expense_claim_payable_account", {}],
 		["default_discount_account", {}],
 		["discount_allowed_account", {"root_type": "Expense"}],
 		["discount_received_account", {"root_type": "Income"}],
diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
index 2b007e9..06a79b4 100644
--- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
+++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
@@ -62,8 +62,13 @@
 		if kwargs['params'].get('date') and kwargs['params'].get('from') and kwargs['params'].get('to'):
 			if test_exchange_values.get(kwargs['params']['date']):
 				return PatchResponse({'result': test_exchange_values[kwargs['params']['date']]}, 200)
+	elif args[0].startswith("https://frankfurter.app") and kwargs.get('params'):
+		if kwargs['params'].get('base') and kwargs['params'].get('symbols'):
+			date = args[0].replace("https://frankfurter.app/", "")
+			if test_exchange_values.get(date):
+				return PatchResponse({'rates': {kwargs['params'].get('symbols'): test_exchange_values.get(date)}}, 200)
 
-	return PatchResponse({'result': None}, 404)
+	return PatchResponse({'rates': None}, 404)
 
 @mock.patch('requests.get', side_effect=patched_requests_get)
 class TestCurrencyExchange(unittest.TestCase):
@@ -102,6 +107,41 @@
 		self.assertFalse(exchange_rate == 60)
 		self.assertEqual(flt(exchange_rate, 3), 65.1)
 
+	def test_exchange_rate_via_exchangerate_host(self, mock_get):
+		save_new_records(test_records)
+
+		# Update Currency Exchange Rate
+		settings = frappe.get_single("Currency Exchange Settings")
+		settings.service_provider = 'exchangerate.host'
+		settings.save()
+
+		# Update exchange
+		frappe.db.set_value("Accounts Settings", None, "allow_stale", 1)
+
+		# Start with allow_stale is True
+		exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying")
+		self.assertEqual(flt(exchange_rate, 3), 60.0)
+
+		exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
+		self.assertEqual(exchange_rate, 65.1)
+
+		exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling")
+		self.assertEqual(exchange_rate, 62.9)
+
+		# Exchange rate as on 15th Dec, 2015
+		self.clear_cache()
+		exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling")
+		self.assertFalse(exchange_rate == 60)
+		self.assertEqual(flt(exchange_rate, 3), 66.999)
+
+		exchange_rate = get_exchange_rate("USD", "INR", "2016-01-20", "for_buying")
+		self.assertFalse(exchange_rate == 60)
+		self.assertEqual(flt(exchange_rate, 3), 65.1)
+
+		settings = frappe.get_single("Currency Exchange Settings")
+		settings.service_provider = 'frankfurter.app'
+		settings.save()
+
 	def test_exchange_rate_strict(self, mock_get):
 		# strict currency settings
 		frappe.db.set_value("Accounts Settings", None, "allow_stale", 0)
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 86c9b3f..bafaab8 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -60,6 +60,22 @@
 
 	frappe.db.set_default("date_format", "dd-mm-yyyy")
 
+	setup_currency_exchange()
+
+def setup_currency_exchange():
+	ces = frappe.get_single('Currency Exchange Settings')
+	try:
+		ces.set('result_key', [])
+		ces.set('req_params', [])
+
+		ces.api_endpoint = "https://frankfurter.app/{transaction_date}"
+		ces.append('result_key', {'key': 'rates'})
+		ces.append('result_key', {'key': '{to_currency}'})
+		ces.append('req_params', {'key': 'base', 'value': '{from_currency}'})
+		ces.append('req_params', {'key': 'symbols', 'value': '{to_currency}'})
+		ces.save()
+	except frappe.ValidationError:
+		pass
 
 def create_compact_item_print_custom_field():
 	create_custom_field('Print Settings', {
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 336b51c..9dbf49e 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -353,7 +353,8 @@
 				"doctype": "UOM",
 				"uom_name": _(d.get("uom_name")),
 				"name": _(d.get("uom_name")),
-				"must_be_whole_number": d.get("must_be_whole_number")
+				"must_be_whole_number": d.get("must_be_whole_number"),
+				"enabled": 1,
 			}).db_insert()
 
 	# bootstrap uom conversion factors
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index cad4c54..4441bb9 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -100,15 +100,21 @@
 
 		if not value:
 			import requests
-			api_url = "https://api.exchangerate.host/convert"
-			response = requests.get(api_url, params={
-				"date": transaction_date,
-				"from": from_currency,
-				"to": to_currency
-			})
+			settings = frappe.get_cached_doc('Currency Exchange Settings')
+			req_params = {
+				"transaction_date": transaction_date,
+				"from_currency": from_currency,
+				"to_currency": to_currency
+			}
+			params = {}
+			for row in settings.req_params:
+				params[row.key] = format_ces_api(row.value, req_params)
+			response = requests.get(format_ces_api(settings.api_endpoint, req_params), params=params)
 			# expire in 6 hours
 			response.raise_for_status()
-			value = response.json()["result"]
+			value = response.json()
+			for res_key in settings.result_key:
+				value = value[format_ces_api(str(res_key.key), req_params)]
 			cache.setex(name=key, time=21600, value=flt(value))
 		return flt(value)
 	except Exception:
@@ -116,6 +122,13 @@
 		frappe.msgprint(_("Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually").format(from_currency, to_currency, transaction_date))
 		return 0.0
 
+def format_ces_api(data, param):
+	return data.format(
+		transaction_date=param.get("transaction_date"),
+		to_currency=param.get("to_currency"),
+		from_currency=param.get("from_currency")
+	)
+
 def enable_all_roles_and_domains():
 	""" enable all roles and domain for testing """
 	# add all roles to users
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index decf522..281e881 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -492,18 +492,20 @@
 		context.shopping_cart = get_product_info_for_website(self.name, skip_quotation_creation=True)
 
 	def add_default_uom_in_conversion_factor_table(self):
-		uom_conv_list = [d.uom for d in self.get("uoms")]
-		if self.stock_uom not in uom_conv_list:
-			ch = self.append('uoms', {})
-			ch.uom = self.stock_uom
-			ch.conversion_factor = 1
+		if not self.is_new() and self.has_value_changed("stock_uom"):
+			self.uoms = []
+			frappe.msgprint(
+				_("Successfully changed Stock UOM, please redefine conversion factors for new UOM."),
+				alert=True,
+			)
 
-		to_remove = []
-		for d in self.get("uoms"):
-			if d.conversion_factor == 1 and d.uom != self.stock_uom:
-				to_remove.append(d)
+		uoms_list = [d.uom for d in self.get("uoms")]
 
-		[self.remove(d) for d in to_remove]
+		if self.stock_uom not in uoms_list:
+			self.append("uoms", {
+				"uom": self.stock_uom,
+				"conversion_factor": 1
+			})
 
 	def update_show_in_website(self):
 		if self.disabled:
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 4028d93..0957ce0 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -584,6 +584,16 @@
 		except frappe.ValidationError as e:
 			self.fail(f"UoM change not allowed even though no SLE / BIN with positive qty exists: {e}")
 
+	def test_erasure_of_old_conversions(self):
+		item = create_item("_item change uom")
+		item.stock_uom = "Gram"
+		item.append("uoms", frappe._dict(uom="Box", conversion_factor=2))
+		item.save()
+		item.reload()
+		item.stock_uom = "Nos"
+		item.save()
+		self.assertEqual(len(item.uoms), 1)
+
 	def test_validate_stock_item(self):
 		self.assertRaises(frappe.ValidationError, validate_is_stock_item, "_Test Non Stock Item")
 
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 93e303c..a61b319 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -8,6 +8,7 @@
 import frappe
 from frappe import _
 from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder.functions import Sum
 from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
 
 import erpnext
@@ -85,8 +86,11 @@
 		self.validate_warehouse()
 		self.validate_work_order()
 		self.validate_bom()
-		self.mark_finished_and_scrap_items()
-		self.validate_finished_goods()
+
+		if self.purpose in ("Manufacture", "Repack"):
+			self.mark_finished_and_scrap_items()
+			self.validate_finished_goods()
+
 		self.validate_with_material_request()
 		self.validate_batch()
 		self.validate_inspection()
@@ -109,8 +113,12 @@
 		self.set_actual_qty()
 		self.calculate_rate_and_amount()
 		self.validate_putaway_capacity()
-		self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
-		self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
+
+		if not self.get("purpose") == "Manufacture":
+			# ignore scrap item wh difference and empty source/target wh
+			# in Manufacture Entry
+			self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
+			self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
 
 	def on_submit(self):
 		self.update_stock_ledger()
@@ -701,26 +709,25 @@
 				validate_bom_no(item_code, d.bom_no)
 
 	def mark_finished_and_scrap_items(self):
-		if self.purpose in ("Repack", "Manufacture"):
-			if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
-				return
+		if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
+			return
 
-			finished_item = self.get_finished_item()
+		finished_item = self.get_finished_item()
 
-			if not finished_item and self.purpose == "Manufacture":
-				# In case of independent Manufacture entry, don't auto set
-				# user must decide and set
-				return
+		if not finished_item and self.purpose == "Manufacture":
+			# In case of independent Manufacture entry, don't auto set
+			# user must decide and set
+			return
 
-			for d in self.items:
-				if d.t_warehouse and not d.s_warehouse:
-					if self.purpose=="Repack" or d.item_code == finished_item:
-						d.is_finished_item = 1
-					else:
-						d.is_scrap_item = 1
+		for d in self.items:
+			if d.t_warehouse and not d.s_warehouse:
+				if self.purpose=="Repack" or d.item_code == finished_item:
+					d.is_finished_item = 1
 				else:
-					d.is_finished_item = 0
-					d.is_scrap_item = 0
+					d.is_scrap_item = 1
+			else:
+				d.is_finished_item = 0
+				d.is_scrap_item = 0
 
 	def get_finished_item(self):
 		finished_item = None
@@ -733,9 +740,9 @@
 
 	def validate_finished_goods(self):
 		"""
-			1. Check if FG exists
-			2. Check if Multiple FG Items are present
-			3. Check FG Item and Qty against WO if present
+			1. Check if FG exists (mfg, repack)
+			2. Check if Multiple FG Items are present (mfg)
+			3. Check FG Item and Qty against WO if present (mfg)
 		"""
 		production_item, wo_qty, finished_items = None, 0, []
 
@@ -748,8 +755,9 @@
 		for d in self.get('items'):
 			if d.is_finished_item:
 				if not self.work_order:
+					# Independent MFG Entry/ Repack Entry, no WO to match against
 					finished_items.append(d.item_code)
-					continue # Independent Manufacture Entry, no WO to match against
+					continue
 
 				if d.item_code != production_item:
 					frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
@@ -762,19 +770,17 @@
 
 				finished_items.append(d.item_code)
 
-		if len(set(finished_items)) > 1:
+		if not finished_items:
 			frappe.throw(
-				msg=_("Multiple items cannot be marked as finished item"),
-				title=_("Note"),
-				exc=FinishedGoodError
+				msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
+				title=_("Missing Finished Good"), exc=FinishedGoodError
 			)
 
 		if self.purpose == "Manufacture":
-			if not finished_items:
+			if len(set(finished_items)) > 1:
 				frappe.throw(
-					msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
-					title=_("Missing Finished Good"),
-					exc=FinishedGoodError
+					msg=_("Multiple items cannot be marked as finished item"),
+					title=_("Note"), exc=FinishedGoodError
 				)
 
 			allowance_percentage = flt(
@@ -1275,22 +1281,29 @@
 		if not self.pro_doc:
 			self.set_work_order_details()
 
-		scrap_items = frappe.db.sql('''
-			SELECT
-				JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description
-			FROM
-				`tabJob Card` JC, `tabJob Card Scrap Item` JCSI
-			WHERE
-				JCSI.parent = JC.name AND JC.docstatus = 1
-				AND JCSI.item_code IS NOT NULL AND JC.work_order = %s
-			GROUP BY
-				JCSI.item_code
-		''', self.work_order, as_dict=1)
-
-		pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty)
-		if pending_qty <=0:
+		if not self.pro_doc.operations:
 			return []
 
+		job_card = frappe.qb.DocType('Job Card')
+		job_card_scrap_item = frappe.qb.DocType('Job Card Scrap Item')
+
+		scrap_items = (
+			frappe.qb.from_(job_card)
+			.select(
+				Sum(job_card_scrap_item.stock_qty).as_('stock_qty'),
+				job_card_scrap_item.item_code, job_card_scrap_item.item_name,
+				job_card_scrap_item.description, job_card_scrap_item.stock_uom)
+			.join(job_card_scrap_item)
+			.on(job_card_scrap_item.parent == job_card.name)
+			.where(
+				(job_card_scrap_item.item_code.isnotnull())
+				& (job_card.work_order == self.work_order)
+				& (job_card.docstatus == 1))
+			.groupby(job_card_scrap_item.item_code)
+		).run(as_dict=1)
+
+		pending_qty = flt(self.get_completed_job_card_qty()) - flt(self.pro_doc.produced_qty)
+
 		used_scrap_items = self.get_used_scrap_items()
 		for row in scrap_items:
 			row.stock_qty -= flt(used_scrap_items.get(row.item_code))
@@ -1304,6 +1317,9 @@
 
 		return scrap_items
 
+	def get_completed_job_card_qty(self):
+		return flt(min([d.completed_qty for d in self.pro_doc.operations]))
+
 	def get_used_scrap_items(self):
 		used_scrap_items = defaultdict(float)
 		data = frappe.get_all(
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index b874874..8f5d442 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -226,9 +226,47 @@
 
 		mtn.cancel()
 
-	def test_repack_no_change_in_valuation(self):
-		company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
+	def test_repack_multiple_fg(self):
+		"Test `is_finished_item` for one item repacked into two items."
+		make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
 
+		repack = frappe.copy_doc(test_records[3])
+		repack.posting_date = nowdate()
+		repack.posting_time = nowtime()
+
+		repack.items[0].qty = 100.0
+		repack.items[0].transfer_qty = 100.0
+		repack.items[1].qty = 50.0
+
+		repack.append("items", {
+			"conversion_factor": 1.0,
+			"cost_center": "_Test Cost Center - _TC",
+			"doctype": "Stock Entry Detail",
+			"expense_account": "Stock Adjustment - _TC",
+			"basic_rate": 150,
+			"item_code": "_Test Item 2",
+			"parentfield": "items",
+			"qty": 50.0,
+			"stock_uom": "_Test UOM",
+			"t_warehouse": "_Test Warehouse - _TC",
+			"transfer_qty": 50.0,
+			"uom": "_Test UOM"
+		})
+		repack.set_stock_entry_type()
+		repack.insert()
+
+		self.assertEqual(repack.items[1].is_finished_item, 1)
+		self.assertEqual(repack.items[2].is_finished_item, 1)
+
+		repack.items[1].is_finished_item = 0
+		repack.items[2].is_finished_item = 0
+
+		# must raise error if 0 fg in repack entry
+		self.assertRaises(FinishedGoodError, repack.validate_finished_goods)
+
+		repack.delete() # teardown
+
+	def test_repack_no_change_in_valuation(self):
 		make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
 		make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
 			qty=50, basic_rate=100)
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 b3348f1..ea617fd 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
@@ -853,7 +853,7 @@
 @frappe.whitelist()
 def get_sla_doctypes():
 	doctypes = []
-	data = frappe.get_list('Service Level Agreement',
+	data = frappe.get_all('Service Level Agreement',
 		{'enabled': 1},
 		['document_type'],
 		distinct=1
diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py
index fbf2594..bc9f04e 100644
--- a/erpnext/tests/utils.py
+++ b/erpnext/tests/utils.py
@@ -125,17 +125,23 @@
 	if default_filters is None:
 		default_filters = {}
 
+	test_filters = []
 	report_execute_fn = frappe.get_attr(get_report_module_dotted_path(module, report_name) + ".execute")
 	report_filters = frappe._dict(default_filters).copy().update(filters)
 
-	report_data = report_execute_fn(report_filters)
+	test_filters.append(report_filters)
 
 	if optional_filters:
 		for key, value in optional_filters.items():
-			filter_with_optional_param = report_filters.copy().update({key: value})
-			report_execute_fn(filter_with_optional_param)
+			test_filters.append(report_filters.copy().update({key: value}))
 
-	return report_data
+	for test_filter in test_filters:
+		try:
+			report_execute_fn(test_filter)
+		except Exception:
+			print(f"Report failed to execute with filters: {test_filter}")
+			raise
+
 
 
 def timeout(seconds=30, error_message="Test timed out."):
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 1d8b3a8..feea228 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -181,8 +181,6 @@
 
 		if len(child_table_values) > 1:
 			self.set(default_field, None)
-		else:
-			self.set(default_field, list(child_table_values)[0])
 
 def delete_events(ref_type, ref_name):
 	events = frappe.db.sql_list(""" SELECT
diff --git a/requirements.txt b/requirements.txt
index faefb77..f447fac 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
 # frappe   # https://github.com/frappe/frappe is installed during bench-init
 gocardless-pro~=1.22.0
-googlemaps  # used in ERPNext, but dependency is defined in Frappe
+googlemaps
 pandas~=1.1.5
 plaid-python~=7.2.1
 pycountry~=20.7.3