Merge branch 'develop' into fix-adjust-asset-value
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 5df8269..22c81dd 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -374,12 +374,13 @@
 			frappe.db.commit()
 		except Exception as e:
 			if frappe.flags.in_test:
+				traceback = frappe.get_traceback()
+				frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
 				raise e
 			else:
 				frappe.db.rollback()
 				traceback = frappe.get_traceback()
-				frappe.log_error(message=traceback)
-
+				frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
 				frappe.flags.deferred_accounting_error = True
 
 def send_mail(deferred_process):
@@ -446,10 +447,12 @@
 
 		if submit:
 			journal_entry.submit()
+
+		frappe.db.commit()
 	except Exception:
 		frappe.db.rollback()
 		traceback = frappe.get_traceback()
-		frappe.log_error(message=traceback)
+		frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
 
 		frappe.flags.deferred_accounting_error = True
 
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index e904768..6b4b43d 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -68,10 +68,12 @@
 	party_details["tax_category"] = get_address_tax_category(party.get("tax_category"),
 		party_address, shipping_address if party_type != "Supplier" else party_address)
 
-	if not party_details.get("taxes_and_charges"):
-		party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company,
-			customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category,
-			billing_address=party_address, shipping_address=shipping_address)
+	tax_template = set_taxes(party.name, party_type, posting_date, company,
+		customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category,
+		billing_address=party_address, shipping_address=shipping_address)
+
+	if tax_template:
+		party_details['taxes_and_charges'] = tax_template
 
 	if cint(fetch_payment_terms_template):
 		party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company)
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 88bcdad..353f908 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -109,7 +109,11 @@
 					invoiced = 0.0,
 					paid = 0.0,
 					credit_note = 0.0,
-					outstanding = 0.0
+					outstanding = 0.0,
+					invoiced_in_account_currency = 0.0,
+					paid_in_account_currency = 0.0,
+					credit_note_in_account_currency = 0.0,
+					outstanding_in_account_currency = 0.0
 				)
 			self.get_invoices(gle)
 
@@ -150,21 +154,28 @@
 		# gle_balance will be the total "debit - credit" for receivable type reports and
 		# and vice-versa for payable type reports
 		gle_balance = self.get_gle_balance(gle)
+		gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle)
+
 		if gle_balance > 0:
 			if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher:
 				# debit against sales / purchase invoice
 				row.paid -= gle_balance
+				row.paid_in_account_currency -= gle_balance_in_account_currency
 			else:
 				# invoice
 				row.invoiced += gle_balance
+				row.invoiced_in_account_currency += gle_balance_in_account_currency
 		else:
 			# payment or credit note for receivables
 			if self.is_invoice(gle):
 				# stand alone debit / credit note
 				row.credit_note -= gle_balance
+				row.credit_note_in_account_currency -= gle_balance_in_account_currency
 			else:
 				# advance / unlinked payment or other adjustment
 				row.paid -= gle_balance
+				row.paid_in_account_currency -= gle_balance_in_account_currency
+
 		if gle.cost_center:
 			row.cost_center =  str(gle.cost_center)
 
@@ -216,8 +227,13 @@
 		# as we can use this to filter out invoices without outstanding
 		for key, row in self.voucher_balance.items():
 			row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision)
+			row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \
+				row.credit_note_in_account_currency, self.currency_precision)
+
 			row.invoice_grand_total = row.invoiced
-			if abs(row.outstanding) > 1.0/10 ** self.currency_precision:
+
+			if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \
+				(abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision):
 				# non-zero oustanding, we must consider this row
 
 				if self.is_invoice(row) and self.filters.based_on_payment_terms:
@@ -583,12 +599,14 @@
 		else:
 			select_fields = "debit, credit"
 
+		doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
+
 		remarks = ", remarks" if self.filters.get("show_remarks") else ""
 
 		self.gl_entries = frappe.db.sql("""
 			select
 				name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
-				against_voucher_type, against_voucher, account_currency, {0} {remarks}
+				against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks}
 			from
 				`tabGL Entry`
 			where
@@ -596,8 +614,8 @@
 				and is_cancelled = 0
 				and party_type=%s
 				and (party is not null and party != '')
-				{1} {2} {3}"""
-			.format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
+				{2} {3} {4}"""
+			.format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
 
 	def get_sales_invoices_or_customers_based_on_sales_person(self):
 		if self.filters.get("sales_person"):
@@ -718,6 +736,13 @@
 		# get the balance of the GL (debit - credit) or reverse balance based on report type
 		return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle)
 
+	def get_gle_balance_in_account_currency(self, gle):
+		# get the balance of the GL (debit - credit) or reverse balance based on report type
+		return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle)
+
+	def get_reverse_balance_in_account_currency(self, gle):
+		return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency')
+
 	def get_reverse_balance(self, gle):
 		# get "credit" balance if report type is "debit" and vice versa
 		return gle.get('debit' if self.dr_or_cr=='credit' else 'credit')
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 9fa50a9..153f5c5 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -80,20 +80,20 @@
 
 		if (frm.doc.docstatus==1) {
 			if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
-				frm.add_custom_button("Transfer Asset", function() {
+				frm.add_custom_button(__("Transfer Asset"), function() {
 					erpnext.asset.transfer_asset(frm);
 				}, __("Manage"));
 
-				frm.add_custom_button("Scrap Asset", function() {
+				frm.add_custom_button(__("Scrap Asset"), function() {
 					erpnext.asset.scrap_asset(frm);
 				}, __("Manage"));
 
-				frm.add_custom_button("Sell Asset", function() {
+				frm.add_custom_button(__("Sell Asset"), function() {
 					frm.trigger("make_sales_invoice");
 				}, __("Manage"));
 
 			} else if (frm.doc.status=='Scrapped') {
-				frm.add_custom_button("Restore Asset", function() {
+				frm.add_custom_button(__("Restore Asset"), function() {
 					erpnext.asset.restore_asset(frm);
 				}, __("Manage"));
 			}
@@ -121,7 +121,7 @@
 			}
 
 			if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
-				frm.add_custom_button("View General Ledger", function() {
+				frm.add_custom_button(__("View General Ledger"), function() {
 					frappe.route_options = {
 						"voucher_no": frm.doc.name,
 						"from_date": frm.doc.available_for_use_date,
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index f2b02cb..5a9f1c2 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -192,8 +192,7 @@
 
 			# value_after_depreciation - current Asset value
 			if self.docstatus == 1 and d.value_after_depreciation:
-				value_after_depreciation = (flt(d.value_after_depreciation) -
-					flt(self.opening_accumulated_depreciation))
+				value_after_depreciation = flt(d.value_after_depreciation)
 			else:
 				value_after_depreciation = (flt(self.gross_purchase_amount) -
 					flt(self.opening_accumulated_depreciation))
@@ -241,7 +240,7 @@
 					break
 
 				# For first row
-				if has_pro_rata and n==0:
+				if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
 					depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
 						self.available_for_use_date, d.depreciation_start_date)
 
@@ -254,7 +253,7 @@
 					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 * cint(d.frequency_of_depreciation))
+							(n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation))
 
 					depreciation_amount_without_pro_rata = depreciation_amount
 
@@ -354,7 +353,12 @@
 	# if it returns True, depreciation_amount will not be equal for the first and last rows
 	def check_is_pro_rata(self, row):
 		has_pro_rata = False
-		days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
+
+		# if not existing asset, from_date = available_for_use_date
+		# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
+		# from_date = 01/01/2022
+		from_date = self.get_modified_available_for_use_date(row)
+		days = date_diff(row.depreciation_start_date, from_date) + 1
 
 		# if frequency_of_depreciation is 12 months, total_days = 365
 		total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
@@ -364,6 +368,9 @@
 
 		return has_pro_rata
 
+	def get_modified_available_for_use_date(self, row):
+		return add_months(self.available_for_use_date, (self.number_of_depreciations_booked * row.frequency_of_depreciation))
+
 	def validate_asset_finance_books(self, row):
 		if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
 			frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
@@ -402,10 +409,11 @@
 
 	# to ensure that final accumulated depreciation amount is accurate
 	def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book):
-		depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book)
+		if not self.opening_accumulated_depreciation:
+			depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book)
 
-		if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata:
-			depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
+			if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata:
+				depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
 
 		return depreciation_amount_for_last_row
 
@@ -850,13 +858,11 @@
 
 @erpnext.allow_regional
 def get_depreciation_amount(asset, depreciable_value, row):
-	depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
-
 	if row.depreciation_method in ("Straight Line", "Manual"):
 		# if the Depreciation Schedule is being prepared for the first time
 		if not asset.flags.increase_in_asset_life:
-			depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) -
-				flt(row.expected_value_after_useful_life)) / depreciation_left
+			depreciation_amount = (flt(asset.gross_purchase_amount) -
+				flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
 
 		# if the Depreciation Schedule is being modified after Asset Repair
 		else:
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index ca10b1d..874fb63 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -57,8 +57,10 @@
 			je.finance_book = d.finance_book
 			je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
 
+			credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account)
+
 			credit_entry = {
-				"account": accumulated_depreciation_account,
+				"account": credit_account,
 				"credit_in_account_currency": d.depreciation_amount,
 				"reference_type": "Asset",
 				"reference_name": asset.name,
@@ -66,7 +68,7 @@
 			}
 
 			debit_entry = {
-				"account": depreciation_expense_account,
+				"account": debit_account,
 				"debit_in_account_currency": d.depreciation_amount,
 				"reference_type": "Asset",
 				"reference_name": asset.name,
@@ -132,6 +134,20 @@
 
 	return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account
 
+def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account):
+	root_type = frappe.get_value("Account", depreciation_expense_account, "root_type")
+
+	if root_type == "Expense":
+		credit_account = accumulated_depreciation_account
+		debit_account = depreciation_expense_account
+	elif root_type == "Income":
+		credit_account = depreciation_expense_account
+		debit_account = accumulated_depreciation_account
+	else:
+		frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account."))
+
+	return credit_account, debit_account
+
 @frappe.whitelist()
 def scrap_asset(asset_name):
 	asset = frappe.get_doc("Asset", asset_name)
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index d1d4527..ce2cb01 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -409,19 +409,18 @@
 			calculate_depreciation = 1,
 			available_for_use_date = "2030-06-06",
 			is_existing_asset = 1,
-			number_of_depreciations_booked = 1,
-			opening_accumulated_depreciation = 40000,
+			number_of_depreciations_booked = 2,
+			opening_accumulated_depreciation = 47095.89,
 			expected_value_after_useful_life = 10000,
-			depreciation_start_date = "2030-12-31",
+			depreciation_start_date = "2032-12-31",
 			total_number_of_depreciations = 3,
 			frequency_of_depreciation = 12
 		)
 
 		self.assertEqual(asset.status, "Draft")
 		expected_schedules = [
-			["2030-12-31", 14246.58, 54246.58],
-			["2031-12-31", 25000.00, 79246.58],
-			["2032-06-06", 10753.42, 90000.00]
+			["2032-12-31", 30000.0, 77095.89],
+			["2033-06-06", 12904.11, 90000.0]
 		]
 		schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
 			for d in asset.get("schedules")]
@@ -869,6 +868,72 @@
 		self.assertFalse(asset.schedules[1].journal_entry)
 		self.assertFalse(asset.schedules[2].journal_entry)
 
+	def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self):
+		"""Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account."""
+
+		asset = create_asset(
+			item_code = "Macbook Pro",
+			calculate_depreciation = 1,
+			available_for_use_date = "2019-12-31",
+			depreciation_start_date = "2020-12-31",
+			frequency_of_depreciation = 12,
+			total_number_of_depreciations = 3,
+			expected_value_after_useful_life = 10000,
+			submit = 1
+		)
+
+		post_depreciation_entries(date="2021-06-01")
+		asset.load_from_db()
+
+		je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
+		accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts]
+
+		for entry in accounting_entries:
+			if entry["account"] == "_Test Depreciations - _TC":
+				self.assertTrue(entry["debit"])
+				self.assertFalse(entry["credit"])
+			else:
+				self.assertTrue(entry["credit"])
+				self.assertFalse(entry["debit"])
+
+	def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self):
+		"""Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account."""
+
+		depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC")
+		depr_expense_account.root_type = "Income"
+		depr_expense_account.parent_account = "Income - _TC"
+		depr_expense_account.save()
+
+		asset = create_asset(
+			item_code = "Macbook Pro",
+			calculate_depreciation = 1,
+			available_for_use_date = "2019-12-31",
+			depreciation_start_date = "2020-12-31",
+			frequency_of_depreciation = 12,
+			total_number_of_depreciations = 3,
+			expected_value_after_useful_life = 10000,
+			submit = 1
+		)
+
+		post_depreciation_entries(date="2021-06-01")
+		asset.load_from_db()
+
+		je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
+		accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts]
+
+		for entry in accounting_entries:
+			if entry["account"] == "_Test Depreciations - _TC":
+				self.assertTrue(entry["credit"])
+				self.assertFalse(entry["debit"])
+			else:
+				self.assertTrue(entry["debit"])
+				self.assertFalse(entry["credit"])
+
+		# resetting
+		depr_expense_account.root_type = "Expense"
+		depr_expense_account.parent_account = "Expenses - _TC"
+		depr_expense_account.save()
+
 	def test_clear_depreciation_schedule(self):
 		"""Tests if clear_depreciation_schedule() works as expected."""
 
diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js
index 51ce157..c702687 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.js
+++ b/erpnext/assets/doctype/asset_category/asset_category.js
@@ -33,7 +33,7 @@
 			var d  = locals[cdt][cdn];
 			return {
 				"filters": {
-					"root_type": "Expense",
+					"root_type": ["in", ["Expense", "Income"]],
 					"is_group": 0,
 					"company": d.company_name
 				}
diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py
index e2f3ca3..bd573bf 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.py
+++ b/erpnext/assets/doctype/asset_category/asset_category.py
@@ -42,10 +42,10 @@
 
 	def validate_account_types(self):
 		account_type_map = {
-			'fixed_asset_account': { 'account_type': 'Fixed Asset' },
-			'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' },
-			'depreciation_expense_account': { 'root_type': 'Expense' },
-			'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' }
+			'fixed_asset_account': {'account_type': ['Fixed Asset']},
+			'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']},
+			'depreciation_expense_account': {'root_type': ['Expense', 'Income']},
+			'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']}
 		}
 		for d in self.accounts:
 			for fieldname in account_type_map.keys():
@@ -53,11 +53,11 @@
 					selected_account = d.get(fieldname)
 					key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type
 					selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match)
-					expected_key_type = account_type_map[fieldname][key_to_match]
+					expected_key_types = account_type_map[fieldname][key_to_match]
 
-					if selected_key_type != expected_key_type:
+					if selected_key_type not in expected_key_types:
 						frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.")
-							.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)),
+							.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)),
 							title=_("Invalid Account"))
 
 	def valide_cwip_account(self):
diff --git a/erpnext/crm/doctype/crm_settings/__init__.py b/erpnext/crm/doctype/crm_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/__init__.py
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.js b/erpnext/crm/doctype/crm_settings/crm_settings.js
new file mode 100644
index 0000000..c6569d8
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('CRM Settings', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json
new file mode 100644
index 0000000..95b19fa
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.json
@@ -0,0 +1,114 @@
+{
+ "actions": [],
+ "creation": "2021-09-09 17:03:22.754446",
+ "description": "Settings for Selling Module",
+ "doctype": "DocType",
+ "document_type": "Other",
+ "engine": "InnoDB",
+ "field_order": [
+  "section_break_5",
+  "campaign_naming_by",
+  "allow_lead_duplication_based_on_emails",
+  "column_break_4",
+  "create_event_on_next_contact_date",
+  "auto_creation_of_contact",
+  "opportunity_section",
+  "close_opportunity_after_days",
+  "column_break_9",
+  "create_event_on_next_contact_date_opportunity",
+  "quotation_section",
+  "default_valid_till"
+ ],
+ "fields": [
+  {
+   "fieldname": "campaign_naming_by",
+   "fieldtype": "Select",
+   "in_list_view": 1,
+   "label": "Campaign Naming By",
+   "options": "Campaign Name\nNaming Series"
+  },
+  {
+   "fieldname": "column_break_9",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "default_valid_till",
+   "fieldtype": "Data",
+   "label": "Default Quotation Validity Days"
+  },
+  {
+   "fieldname": "section_break_5",
+   "fieldtype": "Section Break",
+   "label": "Lead"
+  },
+  {
+   "default": "0",
+   "fieldname": "allow_lead_duplication_based_on_emails",
+   "fieldtype": "Check",
+   "label": "Allow Lead Duplication based on Emails"
+  },
+  {
+   "default": "1",
+   "fieldname": "auto_creation_of_contact",
+   "fieldtype": "Check",
+   "label": "Auto Creation of Contact"
+  },
+  {
+   "default": "1",
+   "fieldname": "create_event_on_next_contact_date",
+   "fieldtype": "Check",
+   "label": "Create Event on Next Contact Date"
+  },
+  {
+   "fieldname": "opportunity_section",
+   "fieldtype": "Section Break",
+   "label": "Opportunity"
+  },
+  {
+   "default": "15",
+   "description": "Auto close Opportunity Replied after the no. of days mentioned above",
+   "fieldname": "close_opportunity_after_days",
+   "fieldtype": "Int",
+   "label": "Close Replied Opportunity After Days"
+  },
+  {
+   "default": "1",
+   "fieldname": "create_event_on_next_contact_date_opportunity",
+   "fieldtype": "Check",
+   "label": "Create Event on Next Contact Date"
+  },
+  {
+   "fieldname": "column_break_4",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "quotation_section",
+   "fieldtype": "Section Break",
+   "label": "Quotation"
+  }
+ ],
+ "icon": "fa fa-cog",
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "migration_hash": "3ae78b12dd1c64d551736c6e82092f90",
+ "modified": "2021-11-03 09:00:36.883496",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "CRM Settings",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py
new file mode 100644
index 0000000..bde5254
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CRMSettings(Document):
+	pass
diff --git a/erpnext/crm/doctype/crm_settings/test_crm_settings.py b/erpnext/crm/doctype/crm_settings/test_crm_settings.py
new file mode 100644
index 0000000..3372c5d
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/test_crm_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+
+class TestCRMSettings(unittest.TestCase):
+	pass
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index c590523..9adbe8b 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -11,6 +11,7 @@
 	cint,
 	comma_and,
 	cstr,
+	get_link_to_form,
 	getdate,
 	has_gravatar,
 	nowdate,
@@ -91,13 +92,14 @@
 			self.contact_doc.save()
 
 	def add_calendar_event(self, opts=None, force=False):
-		super(Lead, self).add_calendar_event({
-			"owner": self.lead_owner,
-			"starts_on": self.contact_date,
-			"ends_on": self.ends_on or "",
-			"subject": ('Contact ' + cstr(self.lead_name)),
-			"description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
-		}, force)
+		if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date'):
+			super(Lead, self).add_calendar_event({
+				"owner": self.lead_owner,
+				"starts_on": self.contact_date,
+				"ends_on": self.ends_on or "",
+				"subject": ('Contact ' + cstr(self.lead_name)),
+				"description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
+			}, force)
 
 	def update_prospects(self):
 		prospects = frappe.get_all('Prospect Lead', filters={'lead': self.name}, fields=['parent'])
@@ -108,12 +110,13 @@
 	def check_email_id_is_unique(self):
 		if self.email_id:
 			# validate email is unique
-			duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]})
-			duplicate_leads = [lead.name for lead in duplicate_leads]
+			if not frappe.db.get_single_value('CRM Settings', 'allow_lead_duplication_based_on_emails'):
+				duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]})
+				duplicate_leads = [frappe.bold(get_link_to_form('Lead', lead.name)) for lead in duplicate_leads]
 
-			if duplicate_leads:
-				frappe.throw(_("Email Address must be unique, already exists for {0}")
-					.format(comma_and(duplicate_leads)), frappe.DuplicateEntryError)
+				if duplicate_leads:
+					frappe.throw(_("Email Address must be unique, already exists for {0}")
+						.format(comma_and(duplicate_leads)), frappe.DuplicateEntryError)
 
 	def on_trash(self):
 		frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
@@ -172,41 +175,42 @@
 		self.title = self.company_name or self.lead_name
 
 	def create_contact(self):
-		if not self.lead_name:
-			self.set_full_name()
-			self.set_lead_name()
+		if frappe.db.get_single_value('CRM Settings', 'auto_creation_of_contact'):
+			if not self.lead_name:
+				self.set_full_name()
+				self.set_lead_name()
 
-		contact = frappe.new_doc("Contact")
-		contact.update({
-			"first_name": self.first_name or self.lead_name,
-			"last_name": self.last_name,
-			"salutation": self.salutation,
-			"gender": self.gender,
-			"designation": self.designation,
-			"company_name": self.company_name,
-		})
-
-		if self.email_id:
-			contact.append("email_ids", {
-				"email_id": self.email_id,
-				"is_primary": 1
+			contact = frappe.new_doc("Contact")
+			contact.update({
+				"first_name": self.first_name or self.lead_name,
+				"last_name": self.last_name,
+				"salutation": self.salutation,
+				"gender": self.gender,
+				"designation": self.designation,
+				"company_name": self.company_name,
 			})
 
-		if self.phone:
-			contact.append("phone_nos", {
-				"phone": self.phone,
-				"is_primary_phone": 1
-			})
+			if self.email_id:
+				contact.append("email_ids", {
+					"email_id": self.email_id,
+					"is_primary": 1
+				})
 
-		if self.mobile_no:
-			contact.append("phone_nos", {
-				"phone": self.mobile_no,
-				"is_primary_mobile_no":1
-			})
+			if self.phone:
+				contact.append("phone_nos", {
+					"phone": self.phone,
+					"is_primary_phone": 1
+				})
 
-		contact.insert(ignore_permissions=True)
+			if self.mobile_no:
+				contact.append("phone_nos", {
+					"phone": self.mobile_no,
+					"is_primary_mobile_no":1
+				})
 
-		return contact
+			contact.insert(ignore_permissions=True)
+
+			return contact
 
 @frappe.whitelist()
 def make_customer(source_name, target_doc=None):
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 0bef80a..fcbd4de 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -8,6 +8,7 @@
 from frappe import _
 from frappe.email.inbox import link_communication_to_document
 from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder import DocType
 from frappe.utils import cint, cstr, flt, get_fullname
 
 from erpnext.setup.utils import get_exchange_rate
@@ -28,7 +29,6 @@
 		})
 
 		self.make_new_lead_if_required()
-
 		self.validate_item_details()
 		self.validate_uom_is_integer("uom", "qty")
 		self.validate_cust_name()
@@ -70,21 +70,21 @@
 		"""Set lead against new opportunity"""
 		if (not self.get("party_name")) and self.contact_email:
 			# check if customer is already created agains the self.contact_email
-			customer = frappe.db.sql("""select
-				distinct `tabDynamic Link`.link_name as customer
-				from
-					`tabContact`,
-					`tabDynamic Link`
-				where `tabContact`.email_id='{0}'
-				and
-					`tabContact`.name=`tabDynamic Link`.parent
-				and
-					ifnull(`tabDynamic Link`.link_name, '')<>''
-				and
-					`tabDynamic Link`.link_doctype='Customer'
-			""".format(self.contact_email), as_dict=True)
-			if customer and customer[0].customer:
-				self.party_name = customer[0].customer
+			dynamic_link, contact = DocType("Dynamic Link"), DocType("Contact")
+			customer = frappe.qb.from_(
+				dynamic_link
+			).join(
+				contact
+			).on(
+				(contact.name == dynamic_link.parent)
+				& (dynamic_link.link_doctype == "Customer")
+				& (contact.email_id == self.contact_email)
+			).select(
+				dynamic_link.link_name
+			).distinct().run(as_dict=True)
+
+			if customer and customer[0].link_name:
+				self.party_name = customer[0].link_name
 				self.opportunity_from = "Customer"
 				return
 
@@ -191,30 +191,31 @@
 		self.add_calendar_event()
 
 	def add_calendar_event(self, opts=None, force=False):
-		if not opts:
-			opts = frappe._dict()
+		if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date_opportunity'):
+			if not opts:
+				opts = frappe._dict()
 
-		opts.description = ""
-		opts.contact_date = self.contact_date
+			opts.description = ""
+			opts.contact_date = self.contact_date
 
-		if self.party_name and self.opportunity_from == 'Customer':
-			if self.contact_person:
-				opts.description = 'Contact '+cstr(self.contact_person)
-			else:
-				opts.description = 'Contact customer '+cstr(self.party_name)
-		elif self.party_name and self.opportunity_from == 'Lead':
-			if self.contact_display:
-				opts.description = 'Contact '+cstr(self.contact_display)
-			else:
-				opts.description = 'Contact lead '+cstr(self.party_name)
+			if self.party_name and self.opportunity_from == 'Customer':
+				if self.contact_person:
+					opts.description = 'Contact '+cstr(self.contact_person)
+				else:
+					opts.description = 'Contact customer '+cstr(self.party_name)
+			elif self.party_name and self.opportunity_from == 'Lead':
+				if self.contact_display:
+					opts.description = 'Contact '+cstr(self.contact_display)
+				else:
+					opts.description = 'Contact lead '+cstr(self.party_name)
 
-		opts.subject = opts.description
-		opts.description += '. By : ' + cstr(self.contact_by)
+			opts.subject = opts.description
+			opts.description += '. By : ' + cstr(self.contact_by)
 
-		if self.to_discuss:
-			opts.description += ' To Discuss : ' + cstr(self.to_discuss)
+			if self.to_discuss:
+				opts.description += ' To Discuss : ' + cstr(self.to_discuss)
 
-		super(Opportunity, self).add_calendar_event(opts, force)
+			super(Opportunity, self).add_calendar_event(opts, force)
 
 	def validate_item_details(self):
 		if not self.get('items'):
@@ -363,7 +364,7 @@
 
 def auto_close_opportunity():
 	""" auto close the `Replied` Opportunities after 7 days """
-	auto_close_after_days = frappe.db.get_single_value("Selling Settings", "close_opportunity_after_days") or 15
+	auto_close_after_days = frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15
 
 	opportunities = frappe.db.sql(""" select name from tabOpportunity where status='Replied' and
 		modified<DATE_SUB(CURDATE(), INTERVAL %s DAY) """, (auto_close_after_days), as_dict=True)
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
index 23ccb7e..ae1f36e 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
@@ -20,7 +20,6 @@
   "configuration_cb",
   "shipping_account_head",
   "section_break_12",
-  "nexus_address",
   "nexus"
  ],
  "fields": [
@@ -87,16 +86,12 @@
    "fieldtype": "Column Break"
   },
   {
+   "depends_on": "nexus",
    "fieldname": "section_break_12",
    "fieldtype": "Section Break",
    "label": "Nexus List"
   },
   {
-   "fieldname": "nexus_address",
-   "fieldtype": "HTML",
-   "label": "Nexus Address"
-  },
-  {
    "fieldname": "nexus",
    "fieldtype": "Table",
    "label": "Nexus",
@@ -108,19 +103,20 @@
    "fieldtype": "Column Break"
   },
   {
-   "fieldname": "column_break_10",
-   "fieldtype": "Column Break"
-  },
-  {
    "fieldname": "company",
    "fieldtype": "Link",
    "label": "Company",
    "options": "Company"
+  },
+  {
+   "fieldname": "column_break_10",
+   "fieldtype": "Column Break"
   }
  ],
  "issingle": 1,
  "links": [],
- "modified": "2021-11-08 18:02:29.232090",
+ "migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f",
+ "modified": "2021-11-30 11:17:24.647979",
  "modified_by": "Administrator",
  "module": "ERPNext Integrations",
  "name": "TaxJar Settings",
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
index b9f24b6..d4bbe88 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
@@ -16,9 +16,9 @@
 class TaxJarSettings(Document):
 
 	def on_update(self):
-		TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
-		TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
-		TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
+		TAXJAR_CREATE_TRANSACTIONS = self.taxjar_create_transactions
+		TAXJAR_CALCULATE_TAX = self.taxjar_calculate_tax
+		TAXJAR_SANDBOX_MODE = self.is_sandbox
 
 		fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
 		fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index db126cd..2ffae1a 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -323,10 +323,14 @@
 		target.maintenance_schedule = source.name
 		target.maintenance_schedule_detail = s_id
 
-	def update_sales(source, target, parent):
+	def update_sales_and_serial(source, target, parent):
 		sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person')
 		target.service_person = sales_person
-		target.serial_no = ''
+		serial_nos = get_serial_nos(target.serial_no)
+		if len(serial_nos) == 1:
+			target.serial_no = serial_nos[0]
+		else:
+			target.serial_no = ''
 
 	doclist = get_mapped_doc("Maintenance Schedule", source_name, {
 		"Maintenance Schedule": {
@@ -342,7 +346,7 @@
 		"Maintenance Schedule Item": {
 			"doctype": "Maintenance Visit Purpose",
 			"condition": lambda doc: doc.item_name == item_name,
-			"postprocess": update_sales
+			"postprocess": update_sales_and_serial
 		}
 	}, target_doc)
 
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
index 7f98354..6f6ca61 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
@@ -43,14 +43,11 @@
 				}
 			});
 		}
-		else {
-			frm.clear_table("purposes");
-		}
-
 		if (!frm.doc.status) {
 			frm.set_value({ status: 'Draft' });
 		}
 		if (frm.doc.__islocal) {
+			frm.clear_table("purposes");
 			frm.set_value({ mntc_date: frappe.datetime.get_today() });
 		}
 	},
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 897e70c..ee9060b 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -278,6 +278,7 @@
 erpnext.patches.v13_0.add_custom_field_for_south_africa #2
 erpnext.patches.v13_0.update_recipient_email_digest
 erpnext.patches.v13_0.shopify_deprecation_warning
+erpnext.patches.v13_0.remove_bad_selling_defaults
 erpnext.patches.v13_0.migrate_stripe_api
 erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
 erpnext.patches.v13_0.einvoicing_deprecation_warning
@@ -312,3 +313,4 @@
 erpnext.patches.v13_0.create_pan_field_for_india #2
 erpnext.patches.v14_0.delete_hub_doctypes
 erpnext.patches.v13_0.create_ksa_vat_custom_fields
+erpnext.patches.v14_0.migrate_crm_settings
diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py
index b3ef5af..5e1df14 100644
--- a/erpnext/patches/v13_0/check_is_income_tax_component.py
+++ b/erpnext/patches/v13_0/check_is_income_tax_component.py
@@ -3,9 +3,9 @@
 
 
 import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_field
 
 import erpnext
-from erpnext.regional.india.setup import setup
 
 
 def execute():
@@ -30,7 +30,14 @@
 		frappe.reload_doc('Regional', 'Report', report)
 
 	if erpnext.get_region() == "India":
-		setup(patch=True)
+		create_custom_field('Salary Component',
+			dict(fieldname='component_type',
+			label='Component Type',
+			fieldtype='Select',
+			insert_after='description',
+			options='\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax',
+			depends_on='eval:doc.type == "Deduction"')
+		)
 
 	if frappe.db.exists("Salary Component", "Income Tax"):
 		frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1)
diff --git a/erpnext/patches/v13_0/remove_bad_selling_defaults.py b/erpnext/patches/v13_0/remove_bad_selling_defaults.py
new file mode 100644
index 0000000..5487a6c
--- /dev/null
+++ b/erpnext/patches/v13_0/remove_bad_selling_defaults.py
@@ -0,0 +1,15 @@
+import frappe
+from frappe import _
+
+
+def execute():
+	selling_settings = frappe.get_single("Selling Settings")
+
+	if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"):
+		selling_settings.customer_group = None
+
+	if selling_settings.territory in (_("All Territories"), "All Territories"):
+		selling_settings.territory = None
+
+	selling_settings.flags.ignore_mandatory=True
+	selling_settings.save(ignore_permissions=True)
diff --git a/erpnext/patches/v14_0/migrate_crm_settings.py b/erpnext/patches/v14_0/migrate_crm_settings.py
new file mode 100644
index 0000000..30d3ea0
--- /dev/null
+++ b/erpnext/patches/v14_0/migrate_crm_settings.py
@@ -0,0 +1,16 @@
+import frappe
+
+
+def execute():
+	settings = frappe.db.get_value('Selling Settings', 'Selling Settings', [
+		'campaign_naming_by',
+		'close_opportunity_after_days',
+		'default_valid_till'
+	], as_dict=True)
+
+	frappe.reload_doc('crm', 'doctype', 'crm_settings')
+	frappe.db.set_value('CRM Settings', 'CRM Settings', {
+		'campaign_naming_by': settings.campaign_naming_by,
+		'close_opportunity_after_days': settings.close_opportunity_after_days,
+		'default_valid_till': settings.default_valid_till
+	})
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 0cfc008..773d53c 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1106,7 +1106,7 @@
 		$.each(this.frm.doc.taxes || [], function(i, d) {
 			if(d.charge_type == "Actual") {
 				frappe.model.set_value(d.doctype, d.name, "tax_amount",
-					flt(d.tax_amount) / flt(exchange_rate));
+					flt(d.base_tax_amount) / flt(exchange_rate));
 			}
 		});
 	}
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
index 9a72410..0f08978 100644
--- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
@@ -82,7 +82,6 @@
 		memberships = frappe.db.get_all('Membership', {
 			'member': self.member,
 			'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
-			'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
 			'membership_status': ('!=', 'Cancelled')
 		}, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date')
 
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 9746fde..fd3ec3c 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -206,26 +206,17 @@
 
 	if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
 		master_doctype = "Sales Taxes and Charges Template"
-		get_tax_template_based_on_category(master_doctype, company, party_details)
-
-		if party_details.get('taxes_and_charges'):
-			return party_details
-
-		if not party_details.company_gstin:
-			return party_details
+		tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
 
 	elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
 		master_doctype = "Purchase Taxes and Charges Template"
-		get_tax_template_based_on_category(master_doctype, company, party_details)
+		tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
 
-		if party_details.get('taxes_and_charges'):
-			return party_details
-
-		if not party_details.supplier_gstin:
-			return party_details
+	if tax_template_by_category:
+		party_details['taxes_and_charges'] = tax_template_by_category
+		return
 
 	if not party_details.place_of_supply: return party_details
-
 	if not party_details.company_gstin: return party_details
 
 	if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
@@ -237,6 +228,7 @@
 
 	if not default_tax:
 		return party_details
+
 	party_details["taxes_and_charges"] = default_tax
 	party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
 
@@ -268,9 +260,7 @@
 	default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')},
 		'name')
 
-	if default_tax:
-		party_details["taxes_and_charges"] = default_tax
-		party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
+	return default_tax
 
 def get_tax_template(master_doctype, company, is_inter_state, state_code):
 	tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'],
@@ -847,13 +837,11 @@
 		doc.get('items')[item_count - 1].taxable_value += diff
 
 def get_depreciation_amount(asset, depreciable_value, row):
-	depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
-
 	if row.depreciation_method in ("Straight Line", "Manual"):
 		# if the Depreciation Schedule is being prepared for the first time
 		if not asset.flags.increase_in_asset_life:
-			depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) -
-				flt(row.expected_value_after_useful_life)) / depreciation_left
+			depreciation_amount = (flt(asset.gross_purchase_amount) -
+				flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
 
 		# if the Depreciation Schedule is being modified after Asset Repair
 		else:
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
index 1051315..7d00d8b 100644
--- a/erpnext/regional/saudi_arabia/utils.py
+++ b/erpnext/regional/saudi_arabia/utils.py
@@ -77,7 +77,7 @@
 			tlv_array.append(''.join([tag, length, value]))
 
 			# Invoice Amount
-			invoice_amount = str(doc.total)
+			invoice_amount = str(doc.grand_total)
 			tag = bytes([4]).hex()
 			length = bytes([len(invoice_amount)]).hex()
 			value = invoice_amount.encode('utf-8').hex()
@@ -101,8 +101,10 @@
 			url = qr_create(base64_string, error='L')
 			url.png(qr_image, scale=2, quiet_zone=1)
 
+			name = frappe.generate_hash(doc.name, 5)
+
 			# making file
-			filename = f"QR-CODE-{doc.name}.png".replace(os.path.sep, "__")
+			filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
 			_file = frappe.get_doc({
 				"doctype": "File",
 				"file_name": filename,
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 769e066..aa83726 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -302,6 +302,109 @@
 
 		enable_calculate_bundle_price(enable=0)
 
+	def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked(self):
+		from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+		from erpnext.stock.doctype.item.test_item import make_item
+
+		make_item("_Test Product Bundle 1", {"is_stock_item": 0})
+		make_item("_Test Product Bundle 2", {"is_stock_item": 0})
+		make_item("_Test Bundle Item 1", {"is_stock_item": 1})
+		make_item("_Test Bundle Item 2", {"is_stock_item": 1})
+		make_item("_Test Bundle Item 3", {"is_stock_item": 1})
+
+		make_product_bundle("_Test Product Bundle 1",
+			["_Test Bundle Item 1", "_Test Bundle Item 2"])
+		make_product_bundle("_Test Product Bundle 2",
+			["_Test Bundle Item 2", "_Test Bundle Item 3"])
+
+		enable_calculate_bundle_price()
+
+		item_list = [
+			{
+				"item_code": "_Test Product Bundle 1",
+				"warehouse": "",
+				"qty": 1,
+				"rate": 400,
+				"delivered_by_supplier": 1,
+				"supplier": '_Test Supplier'
+			},
+			{
+				"item_code": "_Test Product Bundle 2",
+				"warehouse": "",
+				"qty": 1,
+				"rate": 400,
+				"delivered_by_supplier": 1,
+				"supplier": '_Test Supplier'
+			}
+		]
+
+		quotation = make_quotation(item_list=item_list, do_not_submit=1)
+		quotation.packed_items[0].rate = 100
+		quotation.packed_items[1].rate = 200
+		quotation.packed_items[2].rate = 200
+		quotation.packed_items[3].rate = 300
+		quotation.save()
+
+		expected_values = [300, 500]
+
+		for item in quotation.items:
+			self.assertEqual(item.amount, expected_values[item.idx-1])
+
+		enable_calculate_bundle_price(enable=0)
+
+	def test_packed_items_indices_are_reset_when_product_bundle_is_deleted_from_items_table(self):
+		from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+		from erpnext.stock.doctype.item.test_item import make_item
+
+		make_item("_Test Product Bundle 1", {"is_stock_item": 0})
+		make_item("_Test Product Bundle 2", {"is_stock_item": 0})
+		make_item("_Test Product Bundle 3", {"is_stock_item": 0})
+		make_item("_Test Bundle Item 1", {"is_stock_item": 1})
+		make_item("_Test Bundle Item 2", {"is_stock_item": 1})
+		make_item("_Test Bundle Item 3", {"is_stock_item": 1})
+
+		make_product_bundle("_Test Product Bundle 1",
+			["_Test Bundle Item 1", "_Test Bundle Item 2"])
+		make_product_bundle("_Test Product Bundle 2",
+			["_Test Bundle Item 2", "_Test Bundle Item 3"])
+		make_product_bundle("_Test Product Bundle 3",
+			["_Test Bundle Item 3", "_Test Bundle Item 1"])
+
+		item_list = [
+			{
+				"item_code": "_Test Product Bundle 1",
+				"warehouse": "",
+				"qty": 1,
+				"rate": 400,
+				"delivered_by_supplier": 1,
+				"supplier": '_Test Supplier'
+			},
+			{
+				"item_code": "_Test Product Bundle 2",
+				"warehouse": "",
+				"qty": 1,
+				"rate": 400,
+				"delivered_by_supplier": 1,
+				"supplier": '_Test Supplier'
+			},
+			{
+				"item_code": "_Test Product Bundle 3",
+				"warehouse": "",
+				"qty": 1,
+				"rate": 400,
+				"delivered_by_supplier": 1,
+				"supplier": '_Test Supplier'
+			}
+		]
+
+		quotation = make_quotation(item_list=item_list, do_not_submit=1)
+		del quotation.items[1]
+		quotation.save()
+
+		for id, item in enumerate(quotation.packed_items):
+			expected_index = id + 1
+			self.assertEqual(item.idx, expected_index)
+
 test_records = frappe.get_test_records('Quotation')
 
 def enable_calculate_bundle_price(enable=1):
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 47b8ebd..e69e28d 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -925,6 +925,7 @@
 				"supplier",
 				"pricing_rules"
 			],
+			"condition": lambda doc: doc.parent_item in items_to_map
 		}
 	}, target_doc, set_missing_values)
 
@@ -977,6 +978,7 @@
 			description=i['description']
 		)).insert()
 		work_order.set_work_order_operations()
+		work_order.flags.ignore_mandatory = True
 		work_order.save()
 		out.append(work_order)
 
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index c27f1ea..27bc541 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -11,11 +11,6 @@
   "customer_group",
   "column_break_4",
   "territory",
-  "crm_settings_section",
-  "campaign_naming_by",
-  "default_valid_till",
-  "column_break_9",
-  "close_opportunity_after_days",
   "item_price_settings_section",
   "selling_price_list",
   "maintain_same_rate_action",
@@ -44,13 +39,6 @@
    "options": "Customer Name\nNaming Series\nAuto Name"
   },
   {
-   "fieldname": "campaign_naming_by",
-   "fieldtype": "Select",
-   "in_list_view": 1,
-   "label": "Campaign Naming By",
-   "options": "Campaign Name\nNaming Series\nAuto Name"
-  },
-  {
    "fieldname": "customer_group",
    "fieldtype": "Link",
    "in_list_view": 1,
@@ -72,18 +60,6 @@
    "options": "Price List"
   },
   {
-   "default": "15",
-   "description": "Auto close Opportunity after the no. of days mentioned above",
-   "fieldname": "close_opportunity_after_days",
-   "fieldtype": "Int",
-   "label": "Close Opportunity After Days"
-  },
-  {
-   "fieldname": "default_valid_till",
-   "fieldtype": "Data",
-   "label": "Default Quotation Validity Days"
-  },
-  {
    "fieldname": "column_break_5",
    "fieldtype": "Column Break"
   },
@@ -170,15 +146,6 @@
    "fieldtype": "Column Break"
   },
   {
-   "fieldname": "crm_settings_section",
-   "fieldtype": "Section Break",
-   "label": "CRM Settings"
-  },
-  {
-   "fieldname": "column_break_9",
-   "fieldtype": "Column Break"
-  },
-  {
    "fieldname": "item_price_settings_section",
    "fieldtype": "Section Break",
    "label": "Item Price Settings"
@@ -204,7 +171,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2021-09-08 19:38:10.175989",
+ "modified": "2021-09-13 12:32:17.004404",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Selling Settings",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py
index e7c5e76..fb86e61 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.py
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.py
@@ -8,7 +8,6 @@
 from frappe.custom.doctype.property_setter.property_setter import make_property_setter
 from frappe.model.document import Document
 from frappe.utils import cint
-from frappe.utils.nestedset import get_root_of
 
 
 class SellingSettings(Document):
@@ -37,9 +36,3 @@
 		editable_bundle_item_rates = cint(self.editable_bundle_item_rates)
 
 		make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False)
-
-	def set_default_customer_group_and_territory(self):
-		if not self.customer_group:
-			self.customer_group = get_root_of('Customer Group')
-		if not self.territory:
-			self.territory = get_root_of('Territory')
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 503aeac..98f9119 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -303,7 +303,6 @@
 
 def update_selling_defaults():
 	selling_settings = frappe.get_doc("Selling Settings")
-	selling_settings.set_default_customer_group_and_territory()
 	selling_settings.cust_master_name = "Customer Name"
 	selling_settings.so_required = "No"
 	selling_settings.dn_required = "No"
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 1478007..cad4c54 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -53,6 +53,7 @@
 
 	frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
 	enable_all_roles_and_domains()
+	set_defaults_for_tests()
 
 	frappe.db.commit()
 
@@ -127,6 +128,14 @@
 		[d.name for d in domains])
 	add_all_roles_to('Administrator')
 
+def set_defaults_for_tests():
+	from frappe.utils.nestedset import get_root_of
+
+	selling_settings = frappe.get_single("Selling Settings")
+	selling_settings.customer_group = get_root_of("Customer Group")
+	selling_settings.territory = get_root_of("Territory")
+	selling_settings.save()
+
 
 def insert_record(records):
 	for r in records:
diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index 1412acf..e47837f 100644
--- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -10,7 +10,7 @@
  "idx": 0,
  "label": "ERPNext Settings",
  "links": [],
- "modified": "2021-10-26 21:32:55.323591",
+ "modified": "2021-11-05 21:32:55.323591",
  "modified_by": "Administrator",
  "module": "Setup",
  "name": "ERPNext Settings",
@@ -123,6 +123,13 @@
    "label": "Products Settings",
    "link_to": "Products Settings",
    "type": "DocType"
+  },
+  {
+   "doc_view": "",
+   "icon": "crm",
+   "label": "CRM Settings",
+   "link_to": "CRM Settings",
+   "type": "DocType"
   }
  ],
  "title": "ERPNext Settings"
diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py
index ed8c878..0da45a5 100644
--- a/erpnext/startup/boot.py
+++ b/erpnext/startup/boot.py
@@ -22,7 +22,7 @@
 			'customer_group')
 		bootinfo.sysdefaults.allow_stale = cint(frappe.db.get_single_value('Accounts Settings',
 			'allow_stale'))
-		bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('Selling Settings',
+		bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('CRM Settings',
 			'default_valid_till'))
 
 		# if no company, show a dialog box to create a new company
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index da9c66d..a33134b 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -6,7 +6,7 @@
 from frappe.model.document import Document
 from frappe.query_builder import Case
 from frappe.query_builder.functions import Coalesce, Sum
-from frappe.utils import flt, nowdate
+from frappe.utils import flt
 
 
 class Bin(Document):
@@ -127,33 +127,11 @@
 
 
 def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):
-	'''Called from erpnext.stock.utils.update_bin'''
+	"""WARNING: This function is deprecated. Inline this function instead of using it."""
+	from erpnext.stock.stock_ledger import repost_current_voucher
+
 	update_qty(bin_name, args)
-
-	if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
-		from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
-
-		if not args.get("posting_date"):
-			args["posting_date"] = nowdate()
-
-		if args.get("is_cancelled") and via_landed_cost_voucher:
-			return
-
-		# Reposts only current voucher SL Entries
-		# Updates valuation rate, stock value, stock queue for current transaction
-		update_entries_after({
-			"item_code": args.get('item_code'),
-			"warehouse": args.get('warehouse'),
-			"posting_date": args.get("posting_date"),
-			"posting_time": args.get("posting_time"),
-			"voucher_type": args.get("voucher_type"),
-			"voucher_no": args.get("voucher_no"),
-			"sle_id": args.get('name'),
-			"creation": args.get('creation')
-		}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
-
-		# update qty in future sle and Validate negative qty
-		update_qty_in_future_sle(args, allow_negative_stock)
+	repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
 
 def get_bin_details(bin_name):
 	return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty',
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 5469a9f..4f4e691 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -1035,7 +1035,7 @@
  "image_field": "image",
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2021-11-30 02:33:06.572442",
+ "modified": "2021-12-03 08:32:03.869294",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Item",
@@ -1103,7 +1103,7 @@
  "search_fields": "item_name,description,item_group,customer_code",
  "show_name_in_global_search": 1,
  "show_preview_popup": 1,
- "sort_field": "idx desc,modified desc",
+ "sort_field": "modified",
  "sort_order": "DESC",
  "title_field": "item_name",
  "track_changes": 1
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index c9b8a37..decf522 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -724,7 +724,6 @@
 
 	def recalculate_bin_qty(self, new_name):
 		from erpnext.stock.stock_balance import repost_stock
-		frappe.db.auto_commit_on_many_writes = 1
 		existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
 		frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
 
@@ -738,7 +737,6 @@
 			repost_stock(new_name, warehouse)
 
 		frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
-		frappe.db.auto_commit_on_many_writes = 0
 
 	@frappe.whitelist()
 	def copy_specification_from_item_group(self):
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 8b1224b..4028d93 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -534,8 +534,6 @@
 
 	def test_index_creation(self):
 		"check if index is getting created in db"
-		from erpnext.stock.doctype.item.item import on_doctype_update
-		on_doctype_update()
 
 		indices = frappe.db.sql("show index from tabItem", as_dict=1)
 		expected_columns = {"item_code", "item_name", "item_group", "route"}
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index 3f73093..e4091c4 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -108,9 +108,32 @@
 
 	packed_items = doc.get("packed_items")
 	doc.set("packed_items", [])
+
 	for d in packed_items:
 		if d not in delete_list:
-			doc.append("packed_items", d)
+			add_item_to_packing_list(doc, d)
+
+def add_item_to_packing_list(doc, packed_item):
+	doc.append("packed_items", {
+		'parent_item': packed_item.parent_item,
+		'item_code': packed_item.item_code,
+		'item_name': packed_item.item_name,
+		'uom': packed_item.uom,
+		'qty': packed_item.qty,
+		'rate': packed_item.rate,
+		'conversion_factor': packed_item.conversion_factor,
+		'description': packed_item.description,
+		'warehouse': packed_item.warehouse,
+		'batch_no': packed_item.batch_no,
+		'actual_batch_qty': packed_item.actual_batch_qty,
+		'serial_no': packed_item.serial_no,
+		'target_warehouse': packed_item.target_warehouse,
+		'actual_qty': packed_item.actual_qty,
+		'projected_qty': packed_item.projected_qty,
+		'incoming_rate': packed_item.incoming_rate,
+		'prevdoc_doctype': packed_item.prevdoc_doctype,
+		'parent_detail_docname': packed_item.parent_detail_docname
+	})
 
 def update_product_bundle_price(doc, parent_items):
 	"""Updates the prices of Product Bundles based on the rates of the Items in the bundle."""
@@ -128,7 +151,8 @@
 		else:
 			update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price)
 
-			bundle_price = 0
+			bundle_item_rate = bundle_item.rate if bundle_item.rate else 0
+			bundle_price = bundle_item.qty * bundle_item_rate
 			parent_items_index += 1
 
 	# for the last product bundle
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 965a32d..01cceb1 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -54,9 +54,11 @@
 
 	@frappe.whitelist()
 	def restart_reposting(self):
-		self.set_status('Queued')
-		frappe.enqueue(repost, timeout=1800, queue='long',
-			job_name='repost_sle', now=True, doc=self)
+		self.set_status('Queued', write=False)
+		self.current_index = 0
+		self.distinct_item_and_warehouse = None
+		self.items_to_be_repost = None
+		self.db_update()
 
 	def deduplicate_similar_repost(self):
 		""" Deduplicate similar reposts based on item-warehouse-posting combination."""
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index d31e65a..a38dfa5 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -545,7 +545,7 @@
 		scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
 
 		# Get raw materials cost from BOM if multiple material consumption entries
-		if frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True):
+		if not outgoing_items_cost and frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True):
 			bom_items = self.get_bom_raw_materials(finished_item_qty)
 			outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
 
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 0679467..5a9e77e 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -24,7 +24,8 @@
 from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
 	create_stock_reconciliation,
 )
-from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle
+from erpnext.tests.utils import ERPNextTestCase, change_settings
 
 
 def get_sle(**args):
@@ -38,9 +39,10 @@
 		order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition,
 		values, as_dict=1)
 
-class TestStockEntry(unittest.TestCase):
+class TestStockEntry(ERPNextTestCase):
 	def tearDown(self):
 		frappe.set_user("Administrator")
+		frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
 
 	def test_fifo(self):
 		frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -582,6 +584,65 @@
 		self.assertEqual(fg_cost,
 			flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2))
 
+	def test_work_order_manufacture_with_material_consumption(self):
+		from erpnext.manufacturing.doctype.work_order.work_order import (
+			make_stock_entry as _make_stock_entry,
+		)
+		frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1")
+
+		bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item",
+			"is_default": 1, "docstatus": 1})
+
+		work_order = frappe.new_doc("Work Order")
+		work_order.update({
+			"company": "_Test Company",
+			"fg_warehouse": "_Test Warehouse 1 - _TC",
+			"production_item": "_Test FG Item",
+			"bom_no": bom_no,
+			"qty": 1.0,
+			"stock_uom": "_Test UOM",
+			"wip_warehouse": "_Test Warehouse - _TC"
+		})
+		work_order.insert()
+		work_order.submit()
+
+		make_stock_entry(item_code="_Test Item",
+			target="Stores - _TC", qty=10, basic_rate=5000.0)
+		make_stock_entry(item_code="_Test Item Home Desktop 100",
+			target="Stores - _TC", qty=10, basic_rate=1000.0)
+
+
+		s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1))
+		for d in s.get("items"):
+			d.s_warehouse = "Stores - _TC"
+		s.insert()
+		s.submit()
+
+		# When Stock Entry has RM and FG
+		s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1))
+		s.save()
+		rm_cost = 0
+		for d in s.get('items'):
+			if d.s_warehouse:
+				rm_cost += d.amount
+		fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount
+		scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount
+		self.assertEqual(fg_cost,
+			flt(rm_cost - scrap_cost, 2))
+
+		# When Stock Entry has only FG + Scrap
+		s.items.pop(0)
+		s.items.pop(0)
+		s.submit()
+
+		rm_cost = 0
+		for d in s.get('items'):
+			if d.s_warehouse:
+				rm_cost += d.amount
+		self.assertEqual(rm_cost, 0)
+		expected_fg_cost = s.get_basic_rate_for_manufactured_item(1)
+		fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount
+		self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2))
 
 	def test_variant_work_order(self):
 		bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
@@ -868,6 +929,83 @@
 		distributed_costs = [d.additional_cost for d in se.items]
 		self.assertEqual([40.0, 60.0], distributed_costs)
 
+	@change_settings("Stock Settings", {"allow_negative_stock": 0})
+	def test_future_negative_sle(self):
+		# Initialize item, batch, warehouse, opening qty
+		item_code = '_Test Future Neg Item'
+		batch_no = '_Test Future Neg Batch'
+		warehouses = [
+			'_Test Future Neg Warehouse Source',
+			'_Test Future Neg Warehouse Destination'
+		]
+		warehouse_names = initialize_records_for_future_negative_sle_test(
+			item_code, batch_no, warehouses,
+			opening_qty=2, posting_date='2021-07-01'
+		)
+
+		# Executing an illegal sequence should raise an error
+		sequence_of_entries = [
+			dict(item_code=item_code,
+				qty=2,
+				from_warehouse=warehouse_names[0],
+				to_warehouse=warehouse_names[1],
+				batch_no=batch_no,
+				posting_date='2021-07-03',
+				purpose='Material Transfer'),
+			dict(item_code=item_code,
+				qty=2,
+				from_warehouse=warehouse_names[1],
+				to_warehouse=warehouse_names[0],
+				batch_no=batch_no,
+				posting_date='2021-07-04',
+				purpose='Material Transfer'),
+			dict(item_code=item_code,
+				qty=2,
+				from_warehouse=warehouse_names[0],
+				to_warehouse=warehouse_names[1],
+				batch_no=batch_no,
+				posting_date='2021-07-02',          # Illegal SE
+				purpose='Material Transfer')
+		]
+
+		self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries)
+
+	@change_settings("Stock Settings", {"allow_negative_stock": 0})
+	def test_future_negative_sle_batch(self):
+		from erpnext.stock.doctype.batch.test_batch import TestBatch
+
+		# Initialize item, batch, warehouse, opening qty
+		item_code = '_Test MultiBatch Item'
+		TestBatch.make_batch_item(item_code)
+
+		batch_nos = [] # store generate batches
+		warehouse = '_Test Warehouse - _TC'
+
+		se1 = make_stock_entry(
+				item_code=item_code,
+				qty=2,
+				to_warehouse=warehouse,
+				posting_date='2021-09-01',
+				purpose='Material Receipt'
+			)
+		batch_nos.append(se1.items[0].batch_no)
+		se2 = make_stock_entry(
+				item_code=item_code,
+				qty=2,
+				to_warehouse=warehouse,
+				posting_date='2021-09-03',
+				purpose='Material Receipt'
+			)
+		batch_nos.append(se2.items[0].batch_no)
+
+		with self.assertRaises(NegativeStockError) as nse:
+			make_stock_entry(item_code=item_code,
+				qty=1,
+				from_warehouse=warehouse,
+				batch_no=batch_nos[1],
+				posting_date='2021-09-02', # backdated consumption of 2nd batch
+				purpose='Material Issue')
+
 def make_serialized_item(**args):
 	args = frappe._dict(args)
 	se = frappe.copy_doc(test_records[0])
@@ -938,3 +1076,31 @@
 		]
 
 test_records = frappe.get_test_records('Stock Entry')
+
+def initialize_records_for_future_negative_sle_test(
+		item_code, batch_no, warehouses, opening_qty, posting_date):
+	from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch
+	from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+		create_stock_reconciliation,
+	)
+	from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+	TestBatch.make_batch_item(item_code)
+	make_new_batch(item_code=item_code, batch_id=batch_no)
+	warehouse_names = [create_warehouse(w) for w in warehouses]
+	create_stock_reconciliation(
+		purpose='Opening Stock',
+		posting_date=posting_date,
+		posting_time='20:00:20',
+		item_code=item_code,
+		warehouse=warehouse_names[0],
+		valuation_rate=100,
+		qty=opening_qty,
+		batch_no=batch_no,
+	)
+	return warehouse_names
+
+
+def create_stock_entries(sequence_of_entries):
+	for entry_detail in sequence_of_entries:
+		make_stock_entry(**entry_detail)
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 93bca7a..c538307 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -8,7 +8,7 @@
 from frappe import _
 from frappe.core.doctype.role.role import get_users
 from frappe.model.document import Document
-from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate
+from frappe.utils import add_days, cint, formatdate, get_datetime, getdate
 
 from erpnext.accounts.utils import get_fiscal_year
 from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
@@ -43,7 +43,6 @@
 
 	def on_submit(self):
 		self.check_stock_frozen_date()
-		self.actual_amt_check()
 		self.calculate_batch_qty()
 
 		if not self.get("via_landed_cost_voucher"):
@@ -57,18 +56,6 @@
 				"sum(actual_qty)") or 0
 			frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
 
-	def actual_amt_check(self):
-		"""Validate that qty at warehouse for selected batch is >=0"""
-		if self.batch_no and not self.get("allow_negative_stock"):
-			batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty)
-				from `tabStock Ledger Entry`
-				where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""",
-				(self.warehouse, self.item_code, self.batch_no))[0][0])
-
-			if batch_bal_after_transaction < 0:
-				frappe.throw(_("Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3}")
-					.format(self.batch_no, batch_bal_after_transaction, self.item_code, self.warehouse))
-
 	def validate_mandatory(self):
 		mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
 		for k in mandatory:
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index ca92936..26db264 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -33,65 +33,6 @@
 			self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse)
 			self.assertEqual(child_warehouse.is_group, 0)
 
-	def test_warehouse_renaming(self):
-		create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory")
-		account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1")
-		self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account}))
-
-		# Rename with abbr
-		if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"):
-			frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1")
-		frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1")
-
-		self.assertTrue(frappe.db.get_value("Warehouse",
-			filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
-
-		# Rename without abbr
-		if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"):
-			frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1")
-
-		frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3")
-
-		self.assertTrue(frappe.db.get_value("Warehouse",
-			filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
-
-		# Another rename with multiple dashes
-		if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"):
-			frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1")
-		frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company")
-
-	def test_warehouse_merging(self):
-		company = "_Test Company with perpetual inventory"
-		create_warehouse("Test Warehouse for Merging 1", company=company,
-			properties={"parent_warehouse": "All Warehouses - TCP1"})
-		create_warehouse("Test Warehouse for Merging 2", company=company,
-			properties={"parent_warehouse": "All Warehouses - TCP1"})
-
-		make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1",
-			qty=1, rate=100, company=company)
-		make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1",
-			qty=1, rate=100, company=company)
-
-		existing_bin_qty = (
-			cint(frappe.db.get_value("Bin",
-				{"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty"))
-			+ cint(frappe.db.get_value("Bin",
-				{"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty"))
-		)
-
-		frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1",
-			"Test Warehouse for Merging 2 - TCP1", merge=True)
-
-		self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1"))
-
-		bin_qty = frappe.db.get_value("Bin",
-			{"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")
-
-		self.assertEqual(bin_qty, existing_bin_qty)
-
-		self.assertTrue(frappe.db.get_value("Warehouse",
-			filters={"account": "Test Warehouse for Merging 2 - TCP1"}))
-
 	def test_unlinking_warehouse_from_item_defaults(self):
 		company = "_Test Company"
 
diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json
index 9b90932..05076b5 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.json
+++ b/erpnext/stock/doctype/warehouse/warehouse.json
@@ -1,7 +1,6 @@
 {
  "actions": [],
  "allow_import": 1,
- "allow_rename": 1,
  "creation": "2013-03-07 18:50:32",
  "description": "A logical Warehouse against which stock entries are made.",
  "doctype": "DocType",
@@ -245,7 +244,7 @@
  "idx": 1,
  "is_tree": 1,
  "links": [],
- "modified": "2021-04-09 19:54:56.263965",
+ "modified": "2021-12-03 04:40:06.414630",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Warehouse",
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index b9dbc38..9cfad86 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -10,7 +10,6 @@
 from frappe.utils import cint, flt
 from frappe.utils.nestedset import NestedSet
 
-import erpnext
 from erpnext.stock import get_warehouse_account
 
 
@@ -68,57 +67,6 @@
 		return frappe.db.sql("""select name from `tabWarehouse`
 			where parent_warehouse = %s limit 1""", self.name)
 
-	def before_rename(self, old_name, new_name, merge=False):
-		super(Warehouse, self).before_rename(old_name, new_name, merge)
-
-		# Add company abbr if not provided
-		new_warehouse = erpnext.encode_company_abbr(new_name, self.company)
-
-		if merge:
-			if not frappe.db.exists("Warehouse", new_warehouse):
-				frappe.throw(_("Warehouse {0} does not exist").format(new_warehouse))
-
-			if self.company != frappe.db.get_value("Warehouse", new_warehouse, "company"):
-				frappe.throw(_("Both Warehouse must belong to same Company"))
-
-		return new_warehouse
-
-	def after_rename(self, old_name, new_name, merge=False):
-		super(Warehouse, self).after_rename(old_name, new_name, merge)
-
-		new_warehouse_name = self.get_new_warehouse_name_without_abbr(new_name)
-		self.db_set("warehouse_name", new_warehouse_name)
-
-		if merge:
-			self.recalculate_bin_qty(new_name)
-
-	def get_new_warehouse_name_without_abbr(self, name):
-		company_abbr = frappe.get_cached_value('Company',  self.company,  "abbr")
-		parts = name.rsplit(" - ", 1)
-
-		if parts[-1].lower() == company_abbr.lower():
-			name = parts[0]
-
-		return name
-
-	def recalculate_bin_qty(self, new_name):
-		from erpnext.stock.stock_balance import repost_stock
-		frappe.db.auto_commit_on_many_writes = 1
-		existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
-		frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
-
-		repost_stock_for_items = frappe.db.sql_list("""select distinct item_code
-			from tabBin where warehouse=%s""", new_name)
-
-		# Delete all existing bins to avoid duplicate bins for the same item and warehouse
-		frappe.db.sql("delete from `tabBin` where warehouse=%s", new_name)
-
-		for item_code in repost_stock_for_items:
-			repost_stock(item_code, new_name)
-
-		frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
-		frappe.db.auto_commit_on_many_writes = 0
-
 	def convert_to_group_or_ledger(self):
 		if self.is_group:
 			self.convert_to_ledger()
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 9d40982..e95c0fc 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -7,9 +7,10 @@
 import frappe
 from frappe import _
 from frappe.model.meta import get_field_precision
-from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now
+from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
 
 import erpnext
+from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
 from erpnext.stock.utils import (
 	get_incoming_outgoing_rate_for_cancel,
 	get_or_make_bin,
@@ -17,19 +18,15 @@
 )
 
 
-# future reposting
 class NegativeStockError(frappe.ValidationError): pass
 class SerialNoExistsInFutureTransaction(frappe.ValidationError):
 	pass
 
 _exceptions = frappe.local('stockledger_exceptions')
-# _exceptions = []
 
 def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
 	from erpnext.controllers.stock_controller import future_sle_exists
 	if sl_entries:
-		from erpnext.stock.utils import update_bin
-
 		cancel = sl_entries[0].get("is_cancelled")
 		if cancel:
 			validate_cancellation(sl_entries)
@@ -64,7 +61,38 @@
 				# preserve previous_qty_after_transaction for qty reposting
 				args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
 
-			update_bin(args, allow_negative_stock, via_landed_cost_voucher)
+			is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
+			if is_stock_item:
+				bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
+				update_bin_qty(bin_name, args)
+				repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
+			else:
+				frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
+
+def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
+	if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
+		if not args.get("posting_date"):
+			args["posting_date"] = nowdate()
+
+		if args.get("is_cancelled") and via_landed_cost_voucher:
+			return
+
+		# Reposts only current voucher SL Entries
+		# Updates valuation rate, stock value, stock queue for current transaction
+		update_entries_after({
+			"item_code": args.get('item_code'),
+			"warehouse": args.get('warehouse'),
+			"posting_date": args.get("posting_date"),
+			"posting_time": args.get("posting_time"),
+			"voucher_type": args.get("voucher_type"),
+			"voucher_no": args.get("voucher_no"),
+			"sle_id": args.get('name'),
+			"creation": args.get('creation')
+		}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+
+		# update qty in future sle and Validate negative qty
+		update_qty_in_future_sle(args, allow_negative_stock)
+
 
 def get_args_for_future_sle(row):
 	return frappe._dict({
@@ -803,9 +831,9 @@
 	def update_bin(self):
 		# update bin for each warehouse
 		for warehouse, data in self.data.items():
-			bin_record = get_or_make_bin(self.item_code, warehouse)
+			bin_name = get_or_make_bin(self.item_code, warehouse)
 
-			frappe.db.set_value('Bin', bin_record, {
+			frappe.db.set_value('Bin', bin_name, {
 				"valuation_rate": data.valuation_rate,
 				"actual_qty": data.qty_after_transaction,
 				"stock_value": data.stock_value
@@ -1061,17 +1089,36 @@
 	allow_negative_stock = cint(allow_negative_stock) \
 		or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
 
-	if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock:
-		sle = get_future_sle_with_negative_qty(args)
-		if sle:
-			message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
-				abs(sle[0]["qty_after_transaction"]),
-				frappe.get_desk_link('Item', args.item_code),
-				frappe.get_desk_link('Warehouse', args.warehouse),
-				sle[0]["posting_date"], sle[0]["posting_time"],
-				frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"]))
+	if allow_negative_stock:
+		return
+	if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
+		return
 
-			frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+	neg_sle = get_future_sle_with_negative_qty(args)
+	if neg_sle:
+		message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+			abs(neg_sle[0]["qty_after_transaction"]),
+			frappe.get_desk_link('Item', args.item_code),
+			frappe.get_desk_link('Warehouse', args.warehouse),
+			neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
+			frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
+
+		frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+
+
+	if not args.batch_no:
+		return
+
+	neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
+	if neg_batch_sle:
+		message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+			abs(neg_batch_sle[0]["cumulative_total"]),
+			frappe.get_desk_link('Batch', args.batch_no),
+			frappe.get_desk_link('Warehouse', args.warehouse),
+			neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
+			frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
+		frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch")
+
 
 def get_future_sle_with_negative_qty(args):
 	return frappe.db.sql("""
@@ -1090,6 +1137,29 @@
 		limit 1
 	""", args, as_dict=1)
 
+
+def get_future_sle_with_negative_batch_qty(args):
+	return frappe.db.sql("""
+		with batch_ledger as (
+			select
+				posting_date, posting_time, voucher_type, voucher_no,
+				sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
+			from `tabStock Ledger Entry`
+			where
+				item_code = %(item_code)s
+				and warehouse = %(warehouse)s
+				and batch_no=%(batch_no)s
+				and is_cancelled = 0
+			order by posting_date, posting_time, creation
+		)
+		select * from batch_ledger
+		where
+			cumulative_total < 0.0
+			and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
+		limit 1
+	""", args, as_dict=1)
+
+
 def _round_off_if_near_zero(number: float, precision: int = 6) -> float:
 	""" Rounds off the number to zero only if number is close to zero for decimal
 		specified in precision. Precision defaults to 6.
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 8031c58..72d8098 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -187,7 +187,7 @@
 	bin_obj.flags.ignore_permissions = True
 	return bin_obj
 
-def get_or_make_bin(item_code, warehouse) -> str:
+def get_or_make_bin(item_code: str , warehouse: str) -> str:
 	bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
 
 	if not bin_record:
@@ -203,11 +203,12 @@
 	return bin_record
 
 def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
+	"""WARNING: This function is deprecated. Inline this function instead of using it."""
 	from erpnext.stock.doctype.bin.bin import update_stock
 	is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
 	if is_stock_item:
-		bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
-		update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher)
+		bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
+		update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher)
 	else:
 		frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))