Merge branch 'develop' into overseas_gst_tax
diff --git a/.flake8 b/.flake8
index 56c9b9a..5735456 100644
--- a/.flake8
+++ b/.flake8
@@ -28,6 +28,7 @@
     B007,
     B950,
     W191,
+    E124, # closing bracket, irritating while writing QB code
 
 max-line-length = 200
 exclude=.github/helper/semgrep_rules
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 44cea31..51e1d6e 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -2,9 +2,10 @@
 # For license information, please see license.txt
 
 
+from functools import reduce
+
 import frappe
 from frappe.utils import flt
-from six.moves import reduce
 
 from erpnext.controllers.status_updater import StatusUpdater
 
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/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/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index 4559fa9..8e3bd8b 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -5,7 +5,6 @@
 import frappe
 from frappe import _, scrub
 from frappe.utils import cint, flt
-from six import iteritems
 
 from erpnext.accounts.party import get_partywise_advanced_payment_amount
 from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
@@ -40,7 +39,7 @@
 		if self.filters.show_gl_balance:
 			gl_balance_map = get_gl_balance(self.filters.report_date)
 
-		for party, party_dict in iteritems(self.party_total):
+		for party, party_dict in self.party_total.items():
 			if party_dict.outstanding == 0:
 				continue
 
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..2a8e0ed 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",
@@ -483,6 +484,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 +517,7 @@
    "link_fieldname": "asset"
   }
  ],
- "modified": "2022-01-18 12:57:36.741192",
+ "modified": "2022-01-20 12:57:36.741192",
  "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/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
index 8fd4978..d2ac10a 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
@@ -2,13 +2,14 @@
 # For license information, please see license.txt
 
 
+from urllib.parse import urlencode
+
 import frappe
 import requests
 from frappe import _
 from frappe.model.document import Document
 from frappe.utils import get_url_to_form
 from frappe.utils.file_manager import get_file_path
-from six.moves.urllib.parse import urlencode
 
 
 class LinkedInSettings(Document):
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
index 66826ba..03c1a1a 100644
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
+++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
@@ -5,11 +5,11 @@
 import csv
 import math
 import time
+from io import StringIO
 
 import dateutil
 import frappe
 from frappe import _
-from six import StringIO
 
 import erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_api as mws
 
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
index e242ace..a8119ac 100644
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
+++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
@@ -2,13 +2,14 @@
 # For license information, please see license.txt
 
 
+from urllib.parse import urlencode
+
 import frappe
 import gocardless_pro
 from frappe import _
 from frappe.integrations.utils import create_payment_gateway, create_request_log
 from frappe.model.document import Document
 from frappe.utils import call_hook_method, cint, flt, get_url
-from six.moves.urllib.parse import urlencode
 
 
 class GoCardlessSettings(Document):
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
index 8da52f4..309d2cb 100644
--- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
+++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
@@ -2,12 +2,13 @@
 # For license information, please see license.txt
 
 
+from urllib.parse import urlparse
+
 import frappe
 from frappe import _
 from frappe.custom.doctype.custom_field.custom_field import create_custom_field
 from frappe.model.document import Document
 from frappe.utils.nestedset import get_root_of
-from six.moves.urllib.parse import urlparse
 
 
 class WoocommerceSettings(Document):
diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py
index d922d87..30d3948 100644
--- a/erpnext/erpnext_integrations/utils.py
+++ b/erpnext/erpnext_integrations/utils.py
@@ -1,10 +1,10 @@
 import base64
 import hashlib
 import hmac
+from urllib.parse import urlparse
 
 import frappe
 from frappe import _
-from six.moves.urllib.parse import urlparse
 
 from erpnext import get_default_company
 
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/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/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index d953697..045e5bc 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -149,6 +149,7 @@
 		self.set_bom_material_details()
 		self.set_bom_scrap_items_detail()
 		self.validate_materials()
+		self.validate_transfer_against()
 		self.set_routing_operations()
 		self.validate_operations()
 		self.calculate_cost()
@@ -690,6 +691,12 @@
 			if act_pbom and act_pbom[0][0]:
 				frappe.throw(_("Cannot deactivate or cancel BOM as it is linked with other BOMs"))
 
+	def validate_transfer_against(self):
+		if not self.with_operations:
+			self.transfer_material_against = "Work Order"
+		if not self.transfer_material_against and not self.is_new():
+			frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value"))
+
 	def set_routing_operations(self):
 		if self.routing and self.with_operations and not self.operations:
 			self.get_routing()
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 7d90933..53437c8 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -403,6 +403,36 @@
 
 		new_bom.delete()
 
+	def test_valid_transfer_defaults(self):
+		bom_with_op = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1})
+		bom = frappe.copy_doc(frappe.get_doc("BOM", bom_with_op), ignore_no_copy=False)
+
+		# test defaults
+		bom.docstatus = 0
+		bom.transfer_material_against = None
+		bom.insert()
+		self.assertEqual(bom.transfer_material_against, "Work Order")
+
+		bom.reload()
+		bom.transfer_material_against = None
+		with self.assertRaises(frappe.ValidationError):
+			bom.save()
+		bom.reload()
+
+		# test saner default
+		bom.transfer_material_against = "Job Card"
+		bom.with_operations = 0
+		bom.save()
+		self.assertEqual(bom.transfer_material_against, "Work Order")
+
+		# test no value on existing doc
+		bom.transfer_material_against = None
+		bom.with_operations = 0
+		bom.save()
+		self.assertEqual(bom.transfer_material_against, "Work Order")
+		bom.delete()
+
+
 def get_default_bom(item_code="_Test FG Item 2"):
 	return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
 
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 170454c..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,
 )
@@ -65,6 +66,7 @@
 		self.validate_warehouse_belongs_to_company()
 		self.calculate_operating_cost()
 		self.validate_qty()
+		self.validate_transfer_against()
 		self.validate_operation_time()
 		self.status = self.get_status()
 
@@ -72,6 +74,7 @@
 
 		self.set_required_items(reset_only_qty = len(self.get("required_items")))
 
+
 	def validate_sales_order(self):
 		if self.sales_order:
 			self.check_sales_order_on_hold_or_close()
@@ -356,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)
@@ -625,6 +629,16 @@
 		if not self.qty > 0:
 			frappe.throw(_("Quantity to Manufacture must be greater than 0."))
 
+	def validate_transfer_against(self):
+		if not self.docstatus == 1:
+			# let user configure operations until they're ready to submit
+			return
+		if not self.operations:
+			self.transfer_material_against = "Work Order"
+		if not self.transfer_material_against:
+			frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value"))
+
+
 	def validate_operation_time(self):
 		for d in self.operations:
 			if not d.time_in_mins > 0:
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 9f7e733..ad5062f 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -334,3 +334,4 @@
 erpnext.patches.v14_0.delete_agriculture_doctypes
 erpnext.patches.v14_0.rearrange_company_fields
 erpnext.patches.v14_0.update_leave_notification_template
+erpnext.patches.v13_0.update_sane_transfer_against
diff --git a/erpnext/patches/v13_0/update_sane_transfer_against.py b/erpnext/patches/v13_0/update_sane_transfer_against.py
new file mode 100644
index 0000000..a163d38
--- /dev/null
+++ b/erpnext/patches/v13_0/update_sane_transfer_against.py
@@ -0,0 +1,11 @@
+import frappe
+
+
+def execute():
+	bom = frappe.qb.DocType("BOM")
+
+	(frappe.qb
+		.update(bom)
+		.set(bom.transfer_material_against, "Work Order")
+		.where(bom.with_operations == 0)
+	).run()
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/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py
index 2d1e02e..ec271a1 100644
--- a/erpnext/regional/germany/utils/datev/datev_csv.py
+++ b/erpnext/regional/germany/utils/datev/datev_csv.py
@@ -1,11 +1,11 @@
 import datetime
 import zipfile
 from csv import QUOTE_NONNUMERIC
+from io import BytesIO
 
 import frappe
 import pandas as pd
 from frappe import _
-from six import BytesIO
 
 from .datev_constants import DataCategory
 
diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py
index 14d5495..052fb2a 100644
--- a/erpnext/regional/report/datev/test_datev.py
+++ b/erpnext/regional/report/datev/test_datev.py
@@ -1,9 +1,9 @@
 import zipfile
+from io import BytesIO
 from unittest import TestCase
 
 import frappe
 from frappe.utils import cstr, now_datetime, today
-from six import BytesIO
 
 from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
 from erpnext.regional.germany.utils.datev.datev_constants import (
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index c94b346..9f1eb75 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -3,6 +3,7 @@
 
 
 import copy
+from urllib.parse import quote
 
 import frappe
 from frappe import _
@@ -10,7 +11,6 @@
 from frappe.utils.nestedset import NestedSet
 from frappe.website.utils import clear_cache
 from frappe.website.website_generator import WebsiteGenerator
-from six.moves.urllib.parse import quote
 
 from erpnext.shopping_cart.filters import ProductFiltersBuilder
 from erpnext.shopping_cart.product_info import set_product_info_for_website
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):
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js
index fe2417b..ef7c2cc 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.js
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.js
@@ -86,10 +86,10 @@
 	],
 	"formatter": function (value, row, column, data, default_formatter) {
 		value = default_formatter(value, row, column, data);
-		if (column.fieldname == "out_qty" && data.out_qty < 0) {
+		if (column.fieldname == "out_qty" && data && data.out_qty < 0) {
 			value = "<span style='color:red'>" + value + "</span>";
 		}
-		else if (column.fieldname == "in_qty" && data.in_qty > 0) {
+		else if (column.fieldname == "in_qty" && data && data.in_qty > 0) {
 			value = "<span style='color:green'>" + value + "</span>";
 		}