Merge pull request #29480 from nextchamp-saqib/revert-drop-einvoicing

revert: "refactor!: drop e-invoicing integration from erpnext"
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index 5746a84..968137e 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -628,6 +628,26 @@
 		for doc in [si, si1]:
 			doc.delete()
 
+	def test_multiple_pricing_rules_with_min_qty(self):
+		make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
+			apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
+		make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4,
+			apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2")
+
+		si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
+		item = si.items[0]
+		item.stock_qty = 1
+		si.save()
+		self.assertFalse(item.discount_percentage)
+		item.qty = 5
+		item.stock_qty = 5
+		si.save()
+		self.assertEqual(item.discount_percentage, 30)
+		si.delete()
+
+		frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
+		frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
+
 test_dependencies = ["Campaign"]
 
 def make_pricing_rule(**args):
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 02bfc9d..7792590 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -73,7 +73,7 @@
 	for key in sorted(pricing_rule_dict):
 		pricing_rules_list.extend(pricing_rule_dict.get(key))
 
-	return pricing_rules_list or pricing_rules
+	return pricing_rules_list
 
 def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
 	filtered_pricing_rules = []
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 39dfd8d..af6a52a 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -469,7 +469,7 @@
 				let row = frappe.get_doc(d.doctype, d.name)
 				set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
 			});
-			frm.trigger("calculate_timesheet_totals");
+			this.frm.trigger("calculate_timesheet_totals");
 		}
 	}
 };
diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
index 7e51299..792e7d2 100644
--- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
+++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py
@@ -71,7 +71,8 @@
 		if doc.currency != doc.company_currency:
 			shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
 
-		self.add_shipping_rule_to_tax_table(doc, shipping_amount)
+		if shipping_amount:
+			self.add_shipping_rule_to_tax_table(doc, shipping_amount)
 
 	def get_shipping_amount_from_rules(self, value):
 		for condition in self.get("conditions"):
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 153f5c5..f414930 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -108,6 +108,10 @@
 				frm.trigger("create_asset_repair");
 			}, __("Manage"));
 
+			frm.add_custom_button(__("Split Asset"), function() {
+				frm.trigger("split_asset");
+			}, __("Manage"));
+
 			if (frm.doc.status != 'Fully Depreciated') {
 				frm.add_custom_button(__("Adjust Asset Value"), function() {
 					frm.trigger("create_asset_value_adjustment");
@@ -322,6 +326,43 @@
 		});
 	},
 
+	split_asset: function(frm) {
+		const title = __('Split Asset');
+
+		const fields = [
+			{
+				fieldname: 'split_qty',
+				fieldtype: 'Int',
+				label: __('Split Qty'),
+				reqd: 1
+			}
+		];
+
+		let dialog = new frappe.ui.Dialog({
+			title: title,
+			fields: fields
+		});
+
+		dialog.set_primary_action(__('Split'), function() {
+			const dialog_data = dialog.get_values();
+			frappe.call({
+				args: {
+					"asset_name": frm.doc.name,
+					"split_qty": cint(dialog_data.split_qty)
+				},
+				method: "erpnext.assets.doctype.asset.asset.split_asset",
+				callback: function(r) {
+					let doclist = frappe.model.sync(r.message);
+					frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+				}
+			});
+
+			dialog.hide();
+		});
+
+		dialog.show();
+	},
+
 	create_asset_value_adjustment: function(frm) {
 		frappe.call({
 			args: {
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 0a7c041..6e6bbf1 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -3,7 +3,7 @@
  "allow_import": 1,
  "allow_rename": 1,
  "autoname": "naming_series:",
- "creation": "2016-03-01 17:01:27.920130",
+ "creation": "2022-01-18 02:26:55.975005",
  "doctype": "DocType",
  "document_type": "Document",
  "engine": "InnoDB",
@@ -23,6 +23,7 @@
   "asset_name",
   "asset_category",
   "location",
+  "split_from",
   "custodian",
   "department",
   "disposal_date",
@@ -142,6 +143,7 @@
   },
   {
    "allow_on_submit": 1,
+   "fetch_from": "item_code.image",
    "fieldname": "image",
    "fieldtype": "Attach Image",
    "hidden": 1,
@@ -483,6 +485,13 @@
    "label": "Finance Books"
   },
   {
+   "fieldname": "split_from",
+   "fieldtype": "Link",
+   "label": "Split From",
+   "options": "Asset",
+   "read_only": 1
+  },
+  {
    "fieldname": "asset_quantity",
    "fieldtype": "Int",
    "label": "Asset Quantity",
@@ -509,7 +518,7 @@
    "link_fieldname": "asset"
   }
  ],
- "modified": "2022-01-18 12:57:36.741192",
+ "modified": "2022-01-30 20:19:24.680027",
  "modified_by": "Administrator",
  "module": "Assets",
  "name": "Asset",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index ac64a95..6e87426 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -38,7 +38,8 @@
 		self.validate_item()
 		self.validate_cost_center()
 		self.set_missing_values()
-		self.prepare_depreciation_data()
+		if not self.split_from:
+			self.prepare_depreciation_data()
 		self.validate_gross_and_purchase_amount()
 		if self.get("schedules"):
 			self.validate_expected_value_after_useful_life()
@@ -202,143 +203,143 @@
 		start = self.clear_depreciation_schedule()
 
 		for finance_book in self.get('finance_books'):
-			self.validate_asset_finance_books(finance_book)
+			self._make_depreciation_schedule(finance_book, start, date_of_sale)
 
-			# value_after_depreciation - current Asset value
-			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))
+	def _make_depreciation_schedule(self, finance_book, start, date_of_sale):
+		self.validate_asset_finance_books(finance_book)
 
-			finance_book.value_after_depreciation = value_after_depreciation
+		value_after_depreciation = self._get_value_after_depreciation(finance_book)
+		finance_book.value_after_depreciation = value_after_depreciation
 
-			number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
-				cint(self.number_of_depreciations_booked)
+		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(finance_book)
+		has_pro_rata = self.check_is_pro_rata(finance_book)
+		if has_pro_rata:
+			number_of_pending_depreciations += 1
 
-			if has_pro_rata:
-				number_of_pending_depreciations += 1
+		skip_row = False
 
-			skip_row = False
+		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
 
-			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, finance_book)
 
-				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(finance_book.depreciation_start_date,
+					n * cint(finance_book.frequency_of_depreciation))
 
-				if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
-					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, - finance_book.frequency_of_depreciation + 1)
 
-					# 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, - finance_book.frequency_of_depreciation + 1)
-
-				# if asset is being sold
-				if date_of_sale:
-					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": 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:
-					from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
-					depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
-						from_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(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(finance_book.frequency_of_depreciation))
-
-					depreciation_amount_without_pro_rata = depreciation_amount
-
-					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, finance_book.finance_book)
-
-					monthly_schedule_date = add_months(schedule_date, 1)
-					schedule_date = add_days(schedule_date, days)
-					last_schedule_date = schedule_date
-
-				if not depreciation_amount: continue
-				value_after_depreciation -= flt(depreciation_amount,
-					self.precision("gross_purchase_amount"))
-
-				# Adjust depreciation amount in the last period based on the 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 asset is being sold
+			if date_of_sale:
+				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:
-					# With monthly depreciation, each depreciation is divided by months remaining until next date
-					if self.allow_monthly_depreciation:
-						# month range is 1 to 12
-						# 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 finance_book.frequency_of_depreciation
+					self._add_depreciation_row(date_of_sale, depreciation_amount, finance_book.depreciation_method,
+						finance_book.finance_book, finance_book.idx)
 
-						for r in range(month_range):
-							if (has_pro_rata and n == 0):
-								# For first entry of monthly depr
-								if r == 0:
-									days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date)
-									per_day_amt = depreciation_amount / days
-									depreciation_amount_for_current_month = per_day_amt * days_until_first_depr
-									depreciation_amount -= depreciation_amount_for_current_month
-									date = monthly_schedule_date
-									amount = depreciation_amount_for_current_month
-								else:
-									date = add_months(monthly_schedule_date, r)
-									amount = depreciation_amount / (month_range - 1)
-							elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1:
-								# For last entry of monthly depr
-								date = last_schedule_date
-								amount = depreciation_amount / month_range
+				break
+
+			# For first row
+			if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
+				from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
+				depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
+					from_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(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(finance_book.frequency_of_depreciation))
+
+				depreciation_amount_without_pro_rata = depreciation_amount
+
+				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, finance_book.finance_book)
+
+				monthly_schedule_date = add_months(schedule_date, 1)
+				schedule_date = add_days(schedule_date, days)
+				last_schedule_date = schedule_date
+
+			if not depreciation_amount: continue
+			value_after_depreciation -= flt(depreciation_amount,
+				self.precision("gross_purchase_amount"))
+
+			# Adjust depreciation amount in the last period based on the 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:
+				# With monthly depreciation, each depreciation is divided by months remaining until next date
+				if self.allow_monthly_depreciation:
+					# month range is 1 to 12
+					# 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 finance_book.frequency_of_depreciation
+
+					for r in range(month_range):
+						if (has_pro_rata and n == 0):
+							# For first entry of monthly depr
+							if r == 0:
+								days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date)
+								per_day_amt = depreciation_amount / days
+								depreciation_amount_for_current_month = per_day_amt * days_until_first_depr
+								depreciation_amount -= depreciation_amount_for_current_month
+								date = monthly_schedule_date
+								amount = depreciation_amount_for_current_month
 							else:
 								date = add_months(monthly_schedule_date, r)
-								amount = depreciation_amount / month_range
+								amount = depreciation_amount / (month_range - 1)
+						elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1:
+							# For last entry of monthly depr
+							date = last_schedule_date
+							amount = depreciation_amount / month_range
+						else:
+							date = add_months(monthly_schedule_date, r)
+							amount = depreciation_amount / month_range
 
-							self.append("schedules", {
-								"schedule_date": date,
-								"depreciation_amount": amount,
-								"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": finance_book.depreciation_method,
-							"finance_book": finance_book.finance_book,
-							"finance_book_id": finance_book.idx
-						})
+						self._add_depreciation_row(date, amount, finance_book.depreciation_method,
+							finance_book.finance_book, finance_book.idx)
+				else:
+					self._add_depreciation_row(schedule_date, depreciation_amount, finance_book.depreciation_method,
+						finance_book.finance_book, finance_book.idx)
+
+	def _add_depreciation_row(self, schedule_date, depreciation_amount, depreciation_method, finance_book, finance_book_id):
+		self.append("schedules", {
+			"schedule_date": schedule_date,
+			"depreciation_amount": depreciation_amount,
+			"depreciation_method": depreciation_method,
+			"finance_book": finance_book,
+			"finance_book_id": finance_book_id
+		})
+
+	def _get_value_after_depreciation(self, finance_book):
+		# value_after_depreciation - current Asset value
+		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))
+
+		return value_after_depreciation
 
 	# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
 	# JE: Journal Entry, FB: Finance Book
@@ -348,7 +349,6 @@
 		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)
@@ -924,3 +924,113 @@
 		depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
 
 	return depreciation_amount
+
+@frappe.whitelist()
+def split_asset(asset_name, split_qty):
+	asset = frappe.get_doc("Asset", asset_name)
+	split_qty = cint(split_qty)
+
+	if split_qty >= asset.asset_quantity:
+		frappe.throw(_("Split qty cannot be grater than or equal to asset qty"))
+
+	remaining_qty = asset.asset_quantity - split_qty
+
+	new_asset = create_new_asset_after_split(asset, split_qty)
+	update_existing_asset(asset, remaining_qty)
+
+	return new_asset
+
+def update_existing_asset(asset, remaining_qty):
+	remaining_gross_purchase_amount = flt((asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity)
+	opening_accumulated_depreciation = flt((asset.opening_accumulated_depreciation * remaining_qty) / asset.asset_quantity)
+
+	frappe.db.set_value("Asset", asset.name, {
+		'opening_accumulated_depreciation': opening_accumulated_depreciation,
+		'gross_purchase_amount': remaining_gross_purchase_amount,
+		'asset_quantity': remaining_qty
+	})
+
+	for finance_book in asset.get('finance_books'):
+		value_after_depreciation = flt((finance_book.value_after_depreciation * remaining_qty)/asset.asset_quantity)
+		expected_value_after_useful_life = flt((finance_book.expected_value_after_useful_life * remaining_qty)/asset.asset_quantity)
+		frappe.db.set_value('Asset Finance Book', finance_book.name, 'value_after_depreciation', value_after_depreciation)
+		frappe.db.set_value('Asset Finance Book', finance_book.name, 'expected_value_after_useful_life', expected_value_after_useful_life)
+
+	accumulated_depreciation = 0
+
+	for term in asset.get('schedules'):
+		depreciation_amount = flt((term.depreciation_amount * remaining_qty)/asset.asset_quantity)
+		frappe.db.set_value('Depreciation Schedule', term.name, 'depreciation_amount', depreciation_amount)
+		accumulated_depreciation += depreciation_amount
+		frappe.db.set_value('Depreciation Schedule', term.name, 'accumulated_depreciation_amount', accumulated_depreciation)
+
+def create_new_asset_after_split(asset, split_qty):
+	new_asset = frappe.copy_doc(asset)
+	new_gross_purchase_amount = flt((asset.gross_purchase_amount * split_qty) / asset.asset_quantity)
+	opening_accumulated_depreciation = flt((asset.opening_accumulated_depreciation * split_qty) / asset.asset_quantity)
+
+	new_asset.gross_purchase_amount = new_gross_purchase_amount
+	new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
+	new_asset.asset_quantity = split_qty
+	new_asset.split_from = asset.name
+	accumulated_depreciation = 0
+
+	for finance_book in new_asset.get('finance_books'):
+		finance_book.value_after_depreciation = flt((finance_book.value_after_depreciation * split_qty)/asset.asset_quantity)
+		finance_book.expected_value_after_useful_life = flt((finance_book.expected_value_after_useful_life * split_qty)/asset.asset_quantity)
+
+	for term in new_asset.get('schedules'):
+		depreciation_amount = flt((term.depreciation_amount * split_qty)/asset.asset_quantity)
+		term.depreciation_amount = depreciation_amount
+		accumulated_depreciation += depreciation_amount
+		term.accumulated_depreciation_amount = accumulated_depreciation
+
+	new_asset.submit()
+	new_asset.set_status()
+
+	for term in new_asset.get('schedules'):
+		# Update references in JV
+		if term.journal_entry:
+			add_reference_in_jv_on_split(term.journal_entry, new_asset.name, asset.name, term.depreciation_amount)
+
+	return new_asset
+
+def add_reference_in_jv_on_split(entry_name, new_asset_name, old_asset_name, depreciation_amount):
+	journal_entry = frappe.get_doc('Journal Entry', entry_name)
+	entries_to_add = []
+	idx = len(journal_entry.get('accounts')) + 1
+
+	for account in journal_entry.get('accounts'):
+		if account.reference_name == old_asset_name:
+			entries_to_add.append(frappe.copy_doc(account).as_dict())
+			if account.credit:
+				account.credit = account.credit - depreciation_amount
+				account.credit_in_account_currency = account.credit_in_account_currency - \
+					account.exchange_rate * depreciation_amount
+			elif account.debit:
+				account.debit = account.debit - depreciation_amount
+				account.debit_in_account_currency = account.debit_in_account_currency - \
+					account.exchange_rate * depreciation_amount
+
+	for entry in entries_to_add:
+		entry.reference_name = new_asset_name
+		if entry.credit:
+			entry.credit = depreciation_amount
+			entry.credit_in_account_currency = entry.exchange_rate * depreciation_amount
+		elif entry.debit:
+			entry.debit = depreciation_amount
+			entry.debit_in_account_currency = entry.exchange_rate * depreciation_amount
+
+		entry.idx = idx
+		idx += 1
+
+		journal_entry.append('accounts', entry)
+
+	journal_entry.flags.ignore_validate_update_after_submit = True
+	journal_entry.save()
+
+	# Repost GL Entries
+	journal_entry.docstatus = 2
+	journal_entry.make_gl_entries(1)
+	journal_entry.docstatus = 1
+	journal_entry.make_gl_entries()
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index b9545f4..c08dc21 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -7,7 +7,7 @@
 from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
 
 from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
-from erpnext.assets.doctype.asset.asset import make_sales_invoice
+from erpnext.assets.doctype.asset.asset import make_sales_invoice, split_asset
 from erpnext.assets.doctype.asset.depreciation import (
 	post_depreciation_entries,
 	restore_asset,
@@ -245,6 +245,57 @@
 		si.cancel()
 		self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
 
+	def test_asset_splitting(self):
+		asset = create_asset(
+			calculate_depreciation = 1,
+			asset_quantity=10,
+			available_for_use_date = '2020-01-01',
+			purchase_date = '2020-01-01',
+			expected_value_after_useful_life = 0,
+			total_number_of_depreciations = 6,
+			number_of_depreciations_booked = 1,
+			frequency_of_depreciation = 10,
+			depreciation_start_date = '2021-01-01',
+			opening_accumulated_depreciation=20000,
+			gross_purchase_amount=120000,
+			submit = 1
+		)
+
+		post_depreciation_entries(date="2021-01-01")
+
+		self.assertEqual(asset.asset_quantity, 10)
+		self.assertEqual(asset.gross_purchase_amount, 120000)
+		self.assertEqual(asset.opening_accumulated_depreciation, 20000)
+
+		new_asset = split_asset(asset.name, 2)
+		asset.load_from_db()
+
+		self.assertEqual(new_asset.asset_quantity, 2)
+		self.assertEqual(new_asset.gross_purchase_amount, 24000)
+		self.assertEqual(new_asset.opening_accumulated_depreciation, 4000)
+		self.assertEqual(new_asset.split_from, asset.name)
+		self.assertEqual(new_asset.schedules[0].depreciation_amount, 4000)
+		self.assertEqual(new_asset.schedules[1].depreciation_amount, 4000)
+
+		self.assertEqual(asset.asset_quantity, 8)
+		self.assertEqual(asset.gross_purchase_amount, 96000)
+		self.assertEqual(asset.opening_accumulated_depreciation, 16000)
+		self.assertEqual(asset.schedules[0].depreciation_amount, 16000)
+		self.assertEqual(asset.schedules[1].depreciation_amount, 16000)
+
+		journal_entry = asset.schedules[0].journal_entry
+
+		jv = frappe.get_doc('Journal Entry', journal_entry)
+		self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000)
+		self.assertEqual(jv.accounts[1].debit_in_account_currency, 16000)
+		self.assertEqual(jv.accounts[2].credit_in_account_currency, 4000)
+		self.assertEqual(jv.accounts[3].debit_in_account_currency, 4000)
+
+		self.assertEqual(jv.accounts[0].reference_name, asset.name)
+		self.assertEqual(jv.accounts[1].reference_name, asset.name)
+		self.assertEqual(jv.accounts[2].reference_name, new_asset.name)
+		self.assertEqual(jv.accounts[3].reference_name, new_asset.name)
+
 	def test_expense_head(self):
 		pr = make_purchase_receipt(item_code="Macbook Pro",
 			qty=2, rate=200000.0, location="Test Location")
@@ -1197,7 +1248,8 @@
 		"available_for_use_date": args.available_for_use_date or "2020-06-06",
 		"location": args.location or "Test Location",
 		"asset_owner": args.asset_owner or "Company",
-		"is_existing_asset": args.is_existing_asset or 1
+		"is_existing_asset": args.is_existing_asset or 1,
+		"asset_quantity": args.get("asset_quantity") or 1
 	})
 
 	if asset.calculate_depreciation:
diff --git a/erpnext/controllers/employee_boarding_controller.py b/erpnext/controllers/employee_boarding_controller.py
index b8dc92e..ae2c737 100644
--- a/erpnext/controllers/employee_boarding_controller.py
+++ b/erpnext/controllers/employee_boarding_controller.py
@@ -132,13 +132,17 @@
 
 	def on_cancel(self):
 		# delete task project
-		for task in frappe.get_all('Task', filters={'project': self.project}):
+		project = self.project
+		for task in frappe.get_all('Task', filters={'project': project}):
 			frappe.delete_doc('Task', task.name, force=1)
-		frappe.delete_doc('Project', self.project, force=1)
+		frappe.delete_doc('Project', project, force=1)
 		self.db_set('project', '')
 		for activity in self.activities:
 			activity.db_set('task', '')
 
+		frappe.msgprint(_('Linked Project {} and Tasks deleted.').format(
+			project), alert=True, indicator='blue')
+
 
 @frappe.whitelist()
 def get_onboarding_details(parent, parenttype):
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index b97432e..2912d3e 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -77,17 +77,17 @@
 						.format(d.idx, get_link_to_form("Batch", d.get("batch_no"))))
 
 	def clean_serial_nos(self):
+		from erpnext.stock.doctype.serial_no.serial_no import clean_serial_no_string
+
 		for row in self.get("items"):
 			if hasattr(row, "serial_no") and row.serial_no:
-				# replace commas by linefeed
-				row.serial_no = row.serial_no.replace(",", "\n")
+				# remove extra whitespace and store one serial no on each line
+				row.serial_no = clean_serial_no_string(row.serial_no)
 
-				# strip preceeding and succeeding spaces for each SN
-				# (SN could have valid spaces in between e.g. SN - 123 - 2021)
-				serial_no_list = row.serial_no.split("\n")
-				serial_no_list = [sn.strip() for sn in serial_no_list]
-
-				row.serial_no = "\n".join(serial_no_list)
+		for row in self.get('packed_items') or []:
+			if hasattr(row, "serial_no") and row.serial_no:
+				# remove extra whitespace and store one serial no on each line
+				row.serial_no = clean_serial_no_string(row.serial_no)
 
 	def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
 			default_cost_center=None):
diff --git a/erpnext/crm/doctype/campaign/campaign.js b/erpnext/crm/doctype/campaign/campaign.js
index 11bfa74..cac45c6 100644
--- a/erpnext/crm/doctype/campaign/campaign.js
+++ b/erpnext/crm/doctype/campaign/campaign.js
@@ -5,7 +5,7 @@
 	refresh: function(frm) {
 		erpnext.toggle_naming_series();
 
-		if (frm.doc.__islocal) {
+		if (frm.is_new()) {
 			frm.toggle_display("naming_series", frappe.boot.sysdefaults.campaign_naming_by=="Naming Series");
 		} else {
 			cur_frm.add_custom_button(__("View Leads"), function() {
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py
index bde5254..98cf7d8 100644
--- a/erpnext/crm/doctype/crm_settings/crm_settings.py
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.py
@@ -1,9 +1,10 @@
 # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
 
-# import frappe
+import frappe
 from frappe.model.document import Document
 
 
 class CRMSettings(Document):
-	pass
+	def validate(self):
+		frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", ""))
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index f8376e6..8e7d67e 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -24,6 +24,14 @@
 			frm.trigger('set_contact_link');
 		}
 	},
+
+	validate: function(frm) {
+		if (frm.doc.status == "Lost" && !frm.doc.lost_reasons.length) {
+			frm.trigger('set_as_lost_dialog');
+			frappe.throw(__("Lost Reasons are required in case opportunity is Lost."));
+		}
+	},
+
 	contact_date: function(frm) {
 		if(frm.doc.contact_date < frappe.datetime.now_datetime()){
 			frm.set_value("contact_date", "");
@@ -82,7 +90,7 @@
 		frm.trigger('setup_opportunity_from');
 		erpnext.toggle_naming_series();
 
-		if(!doc.__islocal && doc.status!=="Lost") {
+		if(!frm.is_new() && doc.status!=="Lost") {
 			if(doc.with_items){
 				frm.add_custom_button(__('Supplier Quotation'),
 					function() {
@@ -187,11 +195,11 @@
 
 	change_form_labels: function(frm) {
 		let company_currency = erpnext.get_currency(frm.doc.company);
-		frm.set_currency_labels(["base_opportunity_amount", "base_total", "base_grand_total"], company_currency);
-		frm.set_currency_labels(["opportunity_amount", "total", "grand_total"], frm.doc.currency);
+		frm.set_currency_labels(["base_opportunity_amount", "base_total"], company_currency);
+		frm.set_currency_labels(["opportunity_amount", "total"], frm.doc.currency);
 
 		// toggle fields
-		frm.toggle_display(["conversion_rate", "base_opportunity_amount", "base_total", "base_grand_total"],
+		frm.toggle_display(["conversion_rate", "base_opportunity_amount", "base_total"],
 			frm.doc.currency != company_currency);
 	},
 
@@ -209,20 +217,15 @@
 	},
 
 	calculate_total: function(frm) {
-		let total = 0, base_total = 0, grand_total = 0, base_grand_total = 0;
+		let total = 0, base_total = 0;
 		frm.doc.items.forEach(item => {
 			total += item.amount;
 			base_total += item.base_amount;
 		})
 
-		base_grand_total = base_total + frm.doc.base_opportunity_amount;
-		grand_total = total + frm.doc.opportunity_amount;
-
 		frm.set_value({
 			'total': flt(total),
-			'base_total': flt(base_total),
-			'grand_total': flt(grand_total),
-			'base_grand_total': flt(base_grand_total)
+			'base_total': flt(base_total)
 		});
 	}
 
diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json
index dc32d9a..089f2d2 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.json
+++ b/erpnext/crm/doctype/opportunity/opportunity.json
@@ -42,10 +42,8 @@
   "items",
   "section_break_32",
   "base_total",
-  "base_grand_total",
   "column_break_33",
   "total",
-  "grand_total",
   "contact_info",
   "customer_address",
   "address_display",
@@ -476,21 +474,6 @@
    "fieldtype": "Column Break"
   },
   {
-   "fieldname": "base_grand_total",
-   "fieldtype": "Currency",
-   "label": "Grand Total (Company Currency)",
-   "options": "Company:company:default_currency",
-   "print_hide": 1,
-   "read_only": 1
-  },
-  {
-   "fieldname": "grand_total",
-   "fieldtype": "Currency",
-   "label": "Grand Total",
-   "options": "currency",
-   "read_only": 1
-  },
-  {
    "fieldname": "lost_detail_section",
    "fieldtype": "Section Break",
    "label": "Lost Reasons"
@@ -510,7 +493,7 @@
  "icon": "fa fa-info-sign",
  "idx": 195,
  "links": [],
- "modified": "2021-10-21 12:04:30.151379",
+ "modified": "2022-01-29 19:32:26.382896",
  "modified_by": "Administrator",
  "module": "CRM",
  "name": "Opportunity",
@@ -547,6 +530,7 @@
  "show_name_in_global_search": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "subject_field": "title",
  "timeline_field": "party_name",
  "title_field": "title",
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index a4fd765..2d53874 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -69,8 +69,6 @@
 
 		self.total = flt(total)
 		self.base_total = flt(base_total)
-		self.grand_total = flt(self.total) + flt(self.opportunity_amount)
-		self.base_grand_total = flt(self.base_total) + flt(self.base_opportunity_amount)
 
 	def make_new_lead_if_required(self):
 		"""Set lead against new opportunity"""
diff --git a/erpnext/crm/doctype/prospect/prospect.js b/erpnext/crm/doctype/prospect/prospect.js
index 67018e1..8721a5b 100644
--- a/erpnext/crm/doctype/prospect/prospect.js
+++ b/erpnext/crm/doctype/prospect/prospect.js
@@ -3,6 +3,8 @@
 
 frappe.ui.form.on('Prospect', {
 	refresh (frm) {
+		frappe.dynamic_link = { doc: frm.doc, fieldname: "name", doctype: frm.doctype };
+
 		if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) {
 			frm.add_custom_button(__("Customer"), function() {
 				frappe.model.open_mapped_doc({
diff --git a/erpnext/crm/module_onboarding/crm/crm.json b/erpnext/crm/module_onboarding/crm/crm.json
index 8315218..0faad1d 100644
--- a/erpnext/crm/module_onboarding/crm/crm.json
+++ b/erpnext/crm/module_onboarding/crm/crm.json
@@ -16,7 +16,7 @@
  "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/CRM",
  "idx": 0,
  "is_complete": 0,
- "modified": "2020-07-08 14:05:42.644448",
+ "modified": "2022-01-29 20:14:29.502145",
  "modified_by": "Administrator",
  "module": "CRM",
  "name": "CRM",
@@ -33,6 +33,9 @@
   },
   {
    "step": "Create and Send Quotation"
+  },
+  {
+   "step": "CRM Settings"
   }
  ],
  "subtitle": "Lead, Opportunity, Customer, and more.",
diff --git a/erpnext/crm/onboarding_step/create_and_send_quotation/create_and_send_quotation.json b/erpnext/crm/onboarding_step/create_and_send_quotation/create_and_send_quotation.json
index 78f7e4d..f0f50de 100644
--- a/erpnext/crm/onboarding_step/create_and_send_quotation/create_and_send_quotation.json
+++ b/erpnext/crm/onboarding_step/create_and_send_quotation/create_and_send_quotation.json
@@ -5,7 +5,6 @@
  "doctype": "Onboarding Step",
  "idx": 0,
  "is_complete": 0,
- "is_mandatory": 0,
  "is_single": 0,
  "is_skipped": 0,
  "modified": "2020-05-28 21:07:11.461172",
@@ -13,6 +12,7 @@
  "name": "Create and Send Quotation",
  "owner": "Administrator",
  "reference_document": "Quotation",
+ "show_form_tour": 0,
  "show_full_form": 1,
  "title": "Create and Send Quotation",
  "validate_action": 1
diff --git a/erpnext/crm/onboarding_step/create_lead/create_lead.json b/erpnext/crm/onboarding_step/create_lead/create_lead.json
index c45e8b0..cb5cce6 100644
--- a/erpnext/crm/onboarding_step/create_lead/create_lead.json
+++ b/erpnext/crm/onboarding_step/create_lead/create_lead.json
@@ -5,7 +5,6 @@
  "doctype": "Onboarding Step",
  "idx": 0,
  "is_complete": 0,
- "is_mandatory": 0,
  "is_single": 0,
  "is_skipped": 0,
  "modified": "2020-05-28 21:07:01.373403",
@@ -13,6 +12,7 @@
  "name": "Create Lead",
  "owner": "Administrator",
  "reference_document": "Lead",
+ "show_form_tour": 0,
  "show_full_form": 1,
  "title": "Create Lead",
  "validate_action": 1
diff --git a/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json b/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json
index 0ee9317..96e0256 100644
--- a/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json
+++ b/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json
@@ -5,7 +5,6 @@
  "doctype": "Onboarding Step",
  "idx": 0,
  "is_complete": 0,
- "is_mandatory": 0,
  "is_single": 0,
  "is_skipped": 0,
  "modified": "2021-01-21 15:28:52.483839",
@@ -13,6 +12,7 @@
  "name": "Create Opportunity",
  "owner": "Administrator",
  "reference_document": "Opportunity",
+ "show_form_tour": 0,
  "show_full_form": 1,
  "title": "Create Opportunity",
  "validate_action": 1
diff --git a/erpnext/crm/onboarding_step/crm_settings/crm_settings.json b/erpnext/crm/onboarding_step/crm_settings/crm_settings.json
new file mode 100644
index 0000000..555d795
--- /dev/null
+++ b/erpnext/crm/onboarding_step/crm_settings/crm_settings.json
@@ -0,0 +1,21 @@
+{
+ "action": "Go to Page",
+ "creation": "2022-01-29 20:14:24.803844",
+ "description": "# CRM Settings\n\nCRM module\u2019s features are configurable as per your business needs. CRM Settings is the place where you can set your preferences for:\n- Campaign\n- Lead\n- Opportunity\n- Quotation",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 1,
+ "is_skipped": 0,
+ "modified": "2022-01-29 20:14:24.803844",
+ "modified_by": "Administrator",
+ "name": "CRM Settings",
+ "owner": "Administrator",
+ "path": "#crm-settings/CRM%20Settings",
+ "reference_document": "CRM Settings",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "CRM Settings",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/onboarding_step/introduction_to_crm/introduction_to_crm.json b/erpnext/crm/onboarding_step/introduction_to_crm/introduction_to_crm.json
index fa26921a..8871753 100644
--- a/erpnext/crm/onboarding_step/introduction_to_crm/introduction_to_crm.json
+++ b/erpnext/crm/onboarding_step/introduction_to_crm/introduction_to_crm.json
@@ -5,13 +5,13 @@
  "doctype": "Onboarding Step",
  "idx": 0,
  "is_complete": 0,
- "is_mandatory": 0,
  "is_single": 0,
  "is_skipped": 0,
  "modified": "2020-05-14 17:28:16.448676",
  "modified_by": "Administrator",
  "name": "Introduction to CRM",
  "owner": "Administrator",
+ "show_form_tour": 0,
  "show_full_form": 0,
  "title": "Introduction to CRM",
  "validate_action": 1,
diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py
index a1247d9..fb62b04 100644
--- a/erpnext/hr/doctype/employee/employee_dashboard.py
+++ b/erpnext/hr/doctype/employee/employee_dashboard.py
@@ -21,7 +21,7 @@
 			},
 			{
 				'label': _('Lifecycle'),
-				'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance']
+				'items': ['Employee Onboarding', 'Employee Transfer', 'Employee Promotion', 'Employee Grievance']
 			},
 			{
 				'label': _('Exit'),
diff --git a/erpnext/hr/doctype/employee_boarding_activity/employee_boarding_activity.json b/erpnext/hr/doctype/employee_boarding_activity/employee_boarding_activity.json
index 044a5a9..8474bd0 100644
--- a/erpnext/hr/doctype/employee_boarding_activity/employee_boarding_activity.json
+++ b/erpnext/hr/doctype/employee_boarding_activity/employee_boarding_activity.json
@@ -62,6 +62,7 @@
   },
   {
    "default": "0",
+   "depends_on": "eval:['Employee Onboarding', 'Employee Onboarding Template'].includes(doc.parenttype)",
    "description": "Applicable in the case of Employee Onboarding",
    "fieldname": "required_for_employee_creation",
    "fieldtype": "Check",
@@ -93,7 +94,7 @@
  ],
  "istable": 1,
  "links": [],
- "modified": "2021-07-30 15:55:22.470102",
+ "modified": "2022-01-29 14:05:00.543122",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "Employee Boarding Activity",
@@ -102,5 +103,6 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js
index 5d1a024..6fbb54d 100644
--- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js
+++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.js
@@ -3,12 +3,6 @@
 
 frappe.ui.form.on('Employee Onboarding', {
 	setup: function(frm) {
-		frm.add_fetch("employee_onboarding_template", "company", "company");
-		frm.add_fetch("employee_onboarding_template", "department", "department");
-		frm.add_fetch("employee_onboarding_template", "designation", "designation");
-		frm.add_fetch("employee_onboarding_template", "employee_grade", "employee_grade");
-
-
 		frm.set_query("job_applicant", function () {
 			return {
 				filters:{
@@ -71,5 +65,19 @@
 				}
 			});
 		}
+	},
+
+	job_applicant: function(frm) {
+		if (frm.doc.job_applicant) {
+			frappe.db.get_value('Employee', {'job_applicant': frm.doc.job_applicant}, 'name', (r) => {
+				if (r.name) {
+					frm.set_value('employee', r.name);
+				} else {
+					frm.set_value('employee', '');
+				}
+			});
+		} else {
+			frm.set_value('employee', '');
+		}
 	}
 });
diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json
index fd877a6..1d2ea0c 100644
--- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json
+++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.json
@@ -92,6 +92,7 @@
    "options": "Employee Onboarding Template"
   },
   {
+   "fetch_from": "employee_onboarding_template.company",
    "fieldname": "company",
    "fieldtype": "Link",
    "label": "Company",
@@ -99,6 +100,7 @@
    "reqd": 1
   },
   {
+   "fetch_from": "employee_onboarding_template.department",
    "fieldname": "department",
    "fieldtype": "Link",
    "in_list_view": 1,
@@ -106,6 +108,7 @@
    "options": "Department"
   },
   {
+   "fetch_from": "employee_onboarding_template.designation",
    "fieldname": "designation",
    "fieldtype": "Link",
    "in_list_view": 1,
@@ -113,6 +116,7 @@
    "options": "Designation"
   },
   {
+   "fetch_from": "employee_onboarding_template.employee_grade",
    "fieldname": "employee_grade",
    "fieldtype": "Link",
    "label": "Employee Grade",
@@ -170,10 +174,11 @@
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2021-07-30 14:55:04.560683",
+ "modified": "2022-01-29 12:33:57.120384",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "Employee Onboarding",
+ "naming_rule": "Expression (old style)",
  "owner": "Administrator",
  "permissions": [
   {
@@ -194,6 +199,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/employee_onboarding/employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py
index eba2a03..a0939a8 100644
--- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py
@@ -14,10 +14,15 @@
 class EmployeeOnboarding(EmployeeBoardingController):
 	def validate(self):
 		super(EmployeeOnboarding, self).validate()
+		self.set_employee()
 		self.validate_duplicate_employee_onboarding()
 
+	def set_employee(self):
+		if not self.employee:
+			self.employee = frappe.db.get_value('Employee', {'job_applicant': self.job_applicant}, 'name')
+
 	def validate_duplicate_employee_onboarding(self):
-		emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant})
+		emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant, "docstatus": ("!=", 2)})
 		if emp_onboarding and emp_onboarding != self.name:
 			frappe.throw(_("Employee Onboarding: {0} already exists for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant)))
 
diff --git a/erpnext/hr/doctype/interview/interview.py b/erpnext/hr/doctype/interview/interview.py
index 4bb003d..a3b111c 100644
--- a/erpnext/hr/doctype/interview/interview.py
+++ b/erpnext/hr/doctype/interview/interview.py
@@ -7,7 +7,7 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe.utils import cstr, get_datetime, get_link_to_form
+from frappe.utils import cstr, flt, get_datetime, get_link_to_form
 
 
 class DuplicateInterviewRoundError(frappe.ValidationError):
@@ -18,6 +18,7 @@
 		self.validate_duplicate_interview()
 		self.validate_designation()
 		self.validate_overlap()
+		self.set_average_rating()
 
 	def on_submit(self):
 		if self.status not in ['Cleared', 'Rejected']:
@@ -67,6 +68,13 @@
 			overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0]))
 			frappe.throw(overlapping_details, title=_('Overlap'))
 
+	def set_average_rating(self):
+		total_rating = 0
+		for entry in self.interview_details:
+			if entry.average_rating:
+				total_rating += entry.average_rating
+
+		self.average_rating = flt(total_rating / len(self.interview_details) if len(self.interview_details) else 0)
 
 	@frappe.whitelist()
 	def reschedule_interview(self, scheduled_on, from_time, to_time):
diff --git a/erpnext/hr/doctype/interview/test_interview.py b/erpnext/hr/doctype/interview/test_interview.py
index 1a2257a..fdb11af 100644
--- a/erpnext/hr/doctype/interview/test_interview.py
+++ b/erpnext/hr/doctype/interview/test_interview.py
@@ -12,6 +12,7 @@
 
 from erpnext.hr.doctype.designation.test_designation import create_designation
 from erpnext.hr.doctype.interview.interview import DuplicateInterviewRoundError
+from erpnext.hr.doctype.job_applicant.job_applicant import get_interview_details
 from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
 
 
@@ -70,6 +71,20 @@
 		email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
 		self.assertTrue("Subject: Interview Feedback Reminder" in email_queue[0].message)
 
+	def test_get_interview_details_for_applicant_dashboard(self):
+		job_applicant = create_job_applicant()
+		interview = create_interview_and_dependencies(job_applicant.name)
+
+		details = get_interview_details(job_applicant.name)
+		self.assertEqual(details.get('stars'), 5)
+		self.assertEqual(details.get('interviews').get(interview.name), {
+			'name': interview.name,
+			'interview_round': interview.interview_round,
+			'expected_average_rating': interview.expected_average_rating * 5,
+			'average_rating': interview.average_rating * 5,
+			'status': 'Pending'
+		})
+
 	def tearDown(self):
 		frappe.db.rollback()
 
@@ -106,7 +121,8 @@
 	interview_round = frappe.new_doc("Interview Round")
 	interview_round.round_name = name
 	interview_round.interview_type = create_interview_type()
-	interview_round.expected_average_rating = 4
+	# average rating = 4
+	interview_round.expected_average_rating = 0.8
 	if designation:
 		interview_round.designation = designation
 
diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.py b/erpnext/hr/doctype/interview_feedback/interview_feedback.py
index d046458..2ff00c1 100644
--- a/erpnext/hr/doctype/interview_feedback/interview_feedback.py
+++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.py
@@ -57,7 +57,6 @@
 
 	def update_interview_details(self):
 		doc = frappe.get_doc('Interview', self.interview)
-		total_rating = 0
 
 		if self.docstatus == 2:
 			for entry in doc.interview_details:
@@ -72,10 +71,6 @@
 					entry.comments = self.feedback
 					entry.result = self.result
 
-				if entry.average_rating:
-					total_rating += entry.average_rating
-
-		doc.average_rating = flt(total_rating / len(doc.interview_details) if len(doc.interview_details) else 0)
 		doc.save()
 		doc.notify_update()
 
diff --git a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
index d2ec5b9..19c4642 100644
--- a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
+++ b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
@@ -24,7 +24,7 @@
 		create_skill_set(['Leadership'])
 
 		interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings)
-		interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 4})
+		interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 0.8})
 		frappe.set_user(interviewer)
 
 		self.assertRaises(frappe.ValidationError, interview_feedback.save)
@@ -50,7 +50,7 @@
 
 		avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0)
 
-		self.assertEqual(flt(avg_rating, 3), feedback_1.average_rating)
+		self.assertEqual(flt(avg_rating, 2), flt(feedback_1.average_rating, 2))
 
 		avg_on_interview_detail = frappe.db.get_value('Interview Detail', {
 			'parent': feedback_1.interview,
@@ -59,7 +59,7 @@
 		}, 'average_rating')
 
 		# 1. average should be reflected in Interview Detail.
-		self.assertEqual(avg_on_interview_detail, feedback_1.average_rating)
+		self.assertEqual(flt(avg_on_interview_detail, 2), flt(feedback_1.average_rating, 2))
 
 		'''For Second Interviewer Feedback'''
 		interviewer = interview.interview_details[1].interviewer
@@ -97,5 +97,5 @@
 
 	skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"])
 	for d in skills:
-		d["rating"] = random.randint(1, 5)
+		d["rating"] = random.random()
 	return skills
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.js b/erpnext/hr/doctype/job_applicant/job_applicant.js
index d7b1c6c..c1e8257 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.js
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.js
@@ -21,9 +21,9 @@
 
 	create_custom_buttons: function(frm) {
 		if (!frm.doc.__islocal && frm.doc.status !== "Rejected" && frm.doc.status !== "Accepted") {
-			frm.add_custom_button(__("Create Interview"), function() {
+			frm.add_custom_button(__("Interview"), function() {
 				frm.events.create_dialog(frm);
-			});
+			}, __("Create"));
 		}
 
 		if (!frm.doc.__islocal) {
@@ -40,10 +40,10 @@
 					frappe.route_options = {
 						"job_applicant": frm.doc.name,
 						"applicant_name": frm.doc.applicant_name,
-						"designation": frm.doc.job_opening,
+						"designation": frm.doc.job_opening || frm.doc.designation,
 					};
 					frappe.new_doc("Job Offer");
-				});
+				}, __("Create"));
 			}
 		}
 	},
@@ -55,13 +55,16 @@
 				job_applicant: frm.doc.name
 			},
 			callback: function(r) {
-				$("div").remove(".form-dashboard-section.custom");
-				frm.dashboard.add_section(
-					frappe.render_template('job_applicant_dashboard', {
-						data: r.message
-					}),
-					__("Interview Summary")
-				);
+				if (r.message) {
+					$("div").remove(".form-dashboard-section.custom");
+					frm.dashboard.add_section(
+						frappe.render_template("job_applicant_dashboard", {
+							data: r.message.interviews,
+							number_of_stars: r.message.stars
+						}),
+						__("Interview Summary")
+					);
+				}
 			}
 		});
 	},
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py
index 5b3d9bf..ccc21ce 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.py
@@ -81,8 +81,13 @@
 		fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"]
 	)
 	interview_detail_map = {}
+	meta = frappe.get_meta("Interview")
+	number_of_stars = meta.get_options("expected_average_rating") or 5
 
 	for detail in interview_details:
+		detail.expected_average_rating = detail.expected_average_rating * number_of_stars if detail.expected_average_rating else 0
+		detail.average_rating = detail.average_rating * number_of_stars if detail.average_rating else 0
+
 		interview_detail_map[detail.name] = detail
 
-	return interview_detail_map
+	return {"interviews": interview_detail_map, "stars": number_of_stars}
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html
index c286787..734b2fe 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html
+++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html
@@ -17,24 +17,33 @@
 				<td class="text-left"> {%= key %} </td>
 				<td class="text-left"> {%= value["interview_round"] %} </td>
 				<td class="text-left"> {%= value["status"] %} </td>
-				<td class="text-left">
-					{% for (i = 0; i < value["expected_average_rating"]; i++) { %}
-						<span class="fa fa-star " style="color: #F6C35E;"></span>
-					{% } %}
-					{% for (i = 0; i < (5-value["expected_average_rating"]); i++) { %}
-					<span class="fa fa-star " style="color: #E7E9EB;"></span>
-					{% } %}
-				</td>
-				<td class="text-left">
-					{% if(value["average_rating"]){ %}
-						{% for (i = 0; i < value["average_rating"]; i++) { %}
-							<span class="fa fa-star " style="color: #F6C35E;"></span>
-						{% } %}
-						{% for (i = 0; i < (5-value["average_rating"]); i++) { %}
-						<span class="fa fa-star " style="color: #E7E9EB;"></span>
-						{% } %}
-					{% } %}
-				</td>
+				{% let right_class = ''; %}
+				{% let left_class = ''; %}
+
+				{% $.each([value["expected_average_rating"], value["average_rating"]], (_, val) => { %}
+					<td class="text-left">
+						<div class="rating">
+							{% for (let i = 1; i <= number_of_stars; i++) { %}
+								{% if (i <= val) { %}
+									{% right_class = 'star-click'; %}
+								{% } else { %}
+									{% right_class = ''; %}
+								{% } %}
+
+								{% if ((i <= val) || ((i - 0.5) == val)) { %}
+									{% left_class = 'star-click'; %}
+								{% } else { %}
+									{% left_class = ''; %}
+								{% } %}
+
+								<svg class="icon icon-md" data-rating={{i}} viewBox="0 0 24 24" fill="none">
+									<path class="right-half {{ right_class }}" d="M11.9987 3.00011C12.177 3.00011 12.3554 3.09303 12.4471 3.27888L14.8213 8.09112C14.8941 8.23872 15.0349 8.34102 15.1978 8.3647L20.5069 9.13641C20.917 9.19602 21.0807 9.69992 20.7841 9.9892L16.9421 13.7354C16.8243 13.8503 16.7706 14.0157 16.7984 14.1779L17.7053 19.4674C17.7753 19.8759 17.3466 20.1874 16.9798 19.9945L12.2314 17.4973C12.1586 17.459 12.0786 17.4398 11.9987 17.4398V3.00011Z" fill="var(--star-fill)" stroke="var(--star-fill)"/>
+									<path class="left-half {{ left_class }}" d="M11.9987 3.00011C11.8207 3.00011 11.6428 3.09261 11.5509 3.27762L9.15562 8.09836C9.08253 8.24546 8.94185 8.34728 8.77927 8.37075L3.42887 9.14298C3.01771 9.20233 2.85405 9.70811 3.1525 9.99707L7.01978 13.7414C7.13858 13.8564 7.19283 14.0228 7.16469 14.1857L6.25116 19.4762C6.18071 19.8842 6.6083 20.1961 6.97531 20.0045L11.7672 17.5022C11.8397 17.4643 11.9192 17.4454 11.9987 17.4454V3.00011Z" fill="var(--star-fill)" stroke="var(--star-fill)"/>
+								</svg>
+							{% } %}
+						</div>
+					</td>
+				{% }); %}
 			</tr>
 		{% } %}
 	</tbody>
diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py
index 39f4719..072fc73 100644
--- a/erpnext/hr/doctype/job_offer/job_offer.py
+++ b/erpnext/hr/doctype/job_offer/job_offer.py
@@ -78,6 +78,7 @@
 				"doctype": "Employee",
 				"field_map": {
 					"applicant_name": "employee_name",
+					"offer_date": "scheduled_confirmation_date"
 				}}
 		}, target_doc, set_missing_values)
 	return doc
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 93ca805..03e0910 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -31,6 +31,7 @@
 from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life
 from erpnext.stock.doctype.serial_no.serial_no import (
 	auto_make_serial_nos,
+	clean_serial_no_string,
 	get_auto_serial_nos,
 	get_serial_nos,
 )
@@ -358,6 +359,7 @@
 			frappe.delete_doc("Batch", row.name)
 
 	def make_serial_nos(self, args):
+		self.serial_no = clean_serial_no_string(self.serial_no)
 		serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
 		if serial_no_series:
 			self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
index 97e7e0a..72eed5e 100644
--- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
@@ -17,14 +17,12 @@
 			fieldname:"from_date",
 			fieldtype: "Datetime",
 			default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)),
-			reqd: 1
 		},
 		{
 			label: __("To Date"),
 			fieldname:"to_date",
 			fieldtype: "Datetime",
 			default: frappe.datetime.now_datetime(),
-			reqd: 1,
 		},
 		{
 			label: __("Job Card"),
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
index 7741823..88b2117 100644
--- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
@@ -3,46 +3,65 @@
 
 import frappe
 from frappe import _
-from frappe.utils import flt
 
 
 def execute(filters=None):
-	columns, data = [], []
+	return get_columns(filters), get_data(filters)
 
-	columns = get_columns(filters)
-	data = get_data(filters)
-
-	return columns, data
 
 def get_data(report_filters):
 	data = []
 	operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1})
 	if operations:
-		operations = [d.name for d in operations]
-		fields = ["production_item as item_code", "item_name", "work_order", "operation",
-			"workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"]
+		if report_filters.get('operation'):
+			operations = [report_filters.get('operation')]
+		else:
+			operations = [d.name for d in operations]
 
-		filters = get_filters(report_filters, operations)
+		job_card = frappe.qb.DocType("Job Card")
 
-		job_cards = frappe.get_all("Job Card", fields = fields,
-			filters = filters)
+		operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_('operating_cost')
+		item_code = (job_card.production_item).as_('item_code')
 
-		for row in job_cards:
-			row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0)
-			data.append(row)
+		query = (frappe.qb
+					.from_(job_card)
+					.select(job_card.name, job_card.work_order, item_code, job_card.item_name,
+						job_card.operation, job_card.serial_no, job_card.batch_no,
+						job_card.workstation, job_card.total_time_in_mins, job_card.hour_rate,
+						operating_cost)
+					.where(
+						(job_card.docstatus == 1)
+						& (job_card.is_corrective_job_card == 1))
+					.groupby(job_card.name)
+				)
 
+		query = append_filters(query, report_filters, operations, job_card)
+		data = query.run(as_dict=True)
 	return data
 
-def get_filters(report_filters, operations):
-	filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1}
-	for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]:
-		if report_filters.get(field):
-			if field != 'serial_no':
-				filters[field] = report_filters.get(field)
-			else:
-				filters[field] = ('like', '% {} %'.format(report_filters.get(field)))
+def append_filters(query, report_filters, operations, job_card):
+	"""Append optional filters to query builder. """
 
-	return filters
+	for field in ("name", "work_order", "operation", "workstation",
+			"company", "serial_no", "batch_no", "production_item"):
+		if report_filters.get(field):
+			if field == 'serial_no':
+				query = query.where(job_card[field].like('%{}%'.format(report_filters.get(field))))
+			elif field == 'operation':
+				query = query.where(job_card[field].isin(operations))
+			else:
+				query = query.where(job_card[field] == report_filters.get(field))
+
+	if report_filters.get('from_date') or report_filters.get('to_date'):
+		job_card_time_log = frappe.qb.DocType("Job Card Time Log")
+
+		query = query.join(job_card_time_log).on(job_card.name == job_card_time_log.parent)
+		if report_filters.get('from_date'):
+			query = query.where(job_card_time_log.from_time >= report_filters.get('from_date'))
+		if report_filters.get('to_date'):
+			query = query.where(job_card_time_log.to_time <= report_filters.get('to_date'))
+
+	return query
 
 def get_columns(filters):
 	return [
diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py
index 1de4726..9f51ded 100644
--- a/erpnext/manufacturing/report/test_reports.py
+++ b/erpnext/manufacturing/report/test_reports.py
@@ -18,7 +18,7 @@
 	("BOM Operations Time", {}),
 	("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
 	("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
-	("Cost of Poor Quality Report", {}),
+	("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
 	("Downtime Analysis", {}),
 	(
 		"Exponential Smoothing Forecasting",
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index 2570df7..1cda0a0 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -235,13 +235,13 @@
   {
    "fieldname": "actual_start_date",
    "fieldtype": "Data",
-   "label": "Actual Start Date",
+   "label": "Actual Start Date (via Time Sheet)",
    "read_only": 1
   },
   {
    "fieldname": "actual_time",
    "fieldtype": "Float",
-   "label": "Actual Time (in Hours)",
+   "label": "Actual Time (in Hours via Time Sheet)",
    "read_only": 1
   },
   {
@@ -251,7 +251,7 @@
   {
    "fieldname": "actual_end_date",
    "fieldtype": "Date",
-   "label": "Actual End Date",
+   "label": "Actual End Date (via Time Sheet)",
    "oldfieldname": "act_completion_date",
    "oldfieldtype": "Date",
    "read_only": 1
@@ -458,10 +458,11 @@
  "index_web_pages_for_search": 1,
  "links": [],
  "max_attachments": 4,
- "modified": "2021-04-28 16:36:11.654632",
+ "modified": "2022-01-29 13:58:27.712714",
  "modified_by": "Administrator",
  "module": "Projects",
  "name": "Project",
+ "naming_rule": "By \"Naming Series\" field",
  "owner": "Administrator",
  "permissions": [
   {
@@ -499,6 +500,7 @@
  "show_name_in_global_search": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "timeline_field": "customer",
  "title_field": "project_name",
  "track_seen": 1
diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json
index ef4740d..8182208 100644
--- a/erpnext/projects/doctype/task/task.json
+++ b/erpnext/projects/doctype/task/task.json
@@ -249,7 +249,7 @@
   {
    "fieldname": "actual_time",
    "fieldtype": "Float",
-   "label": "Actual Time (in hours)",
+   "label": "Actual Time (in Hours via Time Sheet)",
    "read_only": 1
   },
   {
@@ -397,10 +397,11 @@
  "is_tree": 1,
  "links": [],
  "max_attachments": 5,
- "modified": "2021-04-16 12:46:51.556741",
+ "modified": "2022-01-29 13:58:47.005241",
  "modified_by": "Administrator",
  "module": "Projects",
  "name": "Task",
+ "naming_rule": "Expression (old style)",
  "nsm_parent_field": "parent_task",
  "owner": "Administrator",
  "permissions": [
@@ -421,6 +422,7 @@
  "show_preview_popup": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "timeline_field": "project",
  "title_field": "subject",
  "track_seen": 1
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 597d77c..08270bd 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -592,6 +592,6 @@
 		&& doc.fg_completed_qty
 		&& erpnext.stock.bom
 		&& erpnext.stock.bom.name === doc.bom_no;
-	const itemChecks = !!item;
+	const itemChecks = !!item  && !item.allow_alternative_item;
 	return docChecks && itemChecks;
 }
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index d443f9c..8715ef5 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -219,6 +219,7 @@
 
 	if not party_details.place_of_supply: return party_details
 	if not party_details.company_gstin: return party_details
+	if not party_details.supplier_gstin: return party_details
 
 	if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
 		and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py
index fb86e61..e1ef635 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.py
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.py
@@ -16,7 +16,7 @@
 		self.toggle_editable_rate_for_bundle_items()
 
 	def validate(self):
-		for key in ["cust_master_name", "campaign_naming_by", "customer_group", "territory",
+		for key in ["cust_master_name", "customer_group", "territory",
 			"maintain_same_sales_rate", "editable_price_list_rate", "selling_price_list"]:
 				frappe.db.set_default(key, self.get(key, ""))
 
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 540aca2..bd3b41f 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -486,7 +486,7 @@
 					"options": "Competitor Detail"
 				},
 				{
-					"fieldtype": "Text",
+					"fieldtype": "Small Text",
 					"label": __("Detailed Reason"),
 					"fieldname": "detailed_reason"
 				},
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index ee55af3..ee08e38 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -465,6 +465,13 @@
 	return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n')
 		if s.strip()]
 
+def clean_serial_no_string(serial_no: str) -> str:
+	if not serial_no:
+		return ""
+
+	serial_no_list = get_serial_nos(serial_no)
+	return "\n".join(serial_no_list)
+
 def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
 	for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
 		if args.get(field):