Merge pull request #30304 from deepeshgarg007/enable_ksa_vat_docs

fix: Add permission for KSA VAT documents
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 9d1452b..192099b 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -32,6 +32,8 @@
 		name will be changed using autoname options (in a scheduled job)
 		"""
 		self.name = frappe.generate_hash(txt="", length=10)
+		if self.meta.autoname == "hash":
+			self.to_rename = 0
 
 	def validate(self):
 		self.flags.ignore_submit_comment = True
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
index 6a03948..ea05a35 100644
--- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
@@ -39,12 +39,14 @@
 				"label": __("From Date"),
 				"fieldtype": "Date",
 				"default": frappe.defaults.get_user_default("year_start_date"),
+				"reqd": 1
 			},
 			{
 				"fieldname": "to_date",
 				"label": __("To Date"),
 				"fieldtype": "Date",
 				"default": frappe.defaults.get_user_default("year_end_date"),
+				"reqd": 1
 			},
 			{
 				"fieldname": "finance_book",
@@ -56,6 +58,7 @@
 				"fieldname": "dimension",
 				"label": __("Select Dimension"),
 				"fieldtype": "Select",
+				"default": "Cost Center",
 				"options": get_accounting_dimension_options(),
 				"reqd": 1,
 			},
@@ -70,7 +73,7 @@
 });
 
 function get_accounting_dimension_options() {
-	let options =["", "Cost Center", "Project"];
+	let options =["Cost Center", "Project"];
 	frappe.db.get_list('Accounting Dimension',
 		{fields:['document_type']}).then((res) => {
 			res.forEach((dimension) => {
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
index d547470..0c2b6cb 100644
--- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
@@ -15,20 +15,21 @@
 
 
 def execute(filters=None):
-	validate_filters(filters)
-	dimension_items_list = get_dimension_items_list(filters.dimension, filters.company)
 
-	if not dimension_items_list:
+	validate_filters(filters)
+	dimension_list = get_dimensions(filters)
+
+	if not dimension_list:
 		return [], []
 
-	dimension_items_list = [''.join(d) for d in dimension_items_list]
-	columns = get_columns(dimension_items_list)
-	data = get_data(filters, dimension_items_list)
+	columns = get_columns(dimension_list)
+	data = get_data(filters, dimension_list)
 
 	return columns, data
 
-def get_data(filters, dimension_items_list):
+def get_data(filters, dimension_list):
 	company_currency = erpnext.get_company_currency(filters.company)
+
 	acc = frappe.db.sql("""
 		select
 			name, account_number, parent_account, lft, rgt, root_type,
@@ -51,60 +52,54 @@
 		where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company))
 
 	gl_entries_by_account = {}
-	set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account)
-	format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list)
-	accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list)
-	out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list)
+	set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account)
+	format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list,
+		frappe.scrub(filters.get('dimension')))
+	accumulate_values_into_parents(accounts, accounts_by_name, dimension_list)
+	out = prepare_data(accounts, filters, company_currency, dimension_list)
 	out = filter_out_zero_value_rows(out, parent_children_map)
 
 	return out
 
-def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account):
-	for item in dimension_items_list:
-		condition = get_condition(filters.from_date, item, filters.dimension)
-		if account:
-			condition += " and account in ({})"\
-				.format(", ".join([frappe.db.escape(d) for d in account]))
+def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account):
+	condition = get_condition(filters.get('dimension'))
 
-		gl_filters = {
-			"company": filters.get("company"),
-			"from_date": filters.get("from_date"),
-			"to_date": filters.get("to_date"),
-			"finance_book": cstr(filters.get("finance_book"))
-		}
+	if account:
+		condition += " and account in ({})"\
+			.format(", ".join([frappe.db.escape(d) for d in account]))
 
-		gl_filters['item'] = ''.join(item)
+	gl_filters = {
+		"company": filters.get("company"),
+		"from_date": filters.get("from_date"),
+		"to_date": filters.get("to_date"),
+		"finance_book": cstr(filters.get("finance_book"))
+	}
 
-		if filters.get("include_default_book_entries"):
-			gl_filters["company_fb"] = frappe.db.get_value("Company",
-				filters.company, 'default_finance_book')
+	gl_filters['dimensions'] = set(dimension_list)
 
-		for key, value in filters.items():
-			if value:
-				gl_filters.update({
-					key: value
-				})
+	if filters.get("include_default_book_entries"):
+		gl_filters["company_fb"] = frappe.db.get_value("Company",
+			filters.company, 'default_finance_book')
 
-		gl_entries = frappe.db.sql("""
+	gl_entries = frappe.db.sql("""
 		select
-			posting_date, account, debit, credit, is_opening, fiscal_year,
+			posting_date, account, {dimension}, debit, credit, is_opening, fiscal_year,
 			debit_in_account_currency, credit_in_account_currency, account_currency
 		from
 			`tabGL Entry`
 		where
 			company=%(company)s
 		{condition}
+		and posting_date >= %(from_date)s
 		and posting_date <= %(to_date)s
 		and is_cancelled = 0
 		order by account, posting_date""".format(
-			condition=condition),
-			gl_filters, as_dict=True) #nosec
+		dimension = frappe.scrub(filters.get('dimension')), condition=condition), gl_filters, as_dict=True) #nosec
 
-		for entry in gl_entries:
-			entry['dimension_item'] = ''.join(item)
-			gl_entries_by_account.setdefault(entry.account, []).append(entry)
+	for entry in gl_entries:
+		gl_entries_by_account.setdefault(entry.account, []).append(entry)
 
-def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list):
+def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list, dimension_type):
 
 	for entries in gl_entries_by_account.values():
 		for entry in entries:
@@ -114,11 +109,12 @@
 					_("Could not retrieve information for {0}.").format(entry.account), title="Error",
 					raise_exception=1
 				)
-			for item in dimension_items_list:
-				if item == entry.dimension_item:
-					d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit)
 
-def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list):
+			for dimension in dimension_list:
+				if dimension == entry.get(dimension_type):
+					d[frappe.scrub(dimension)] = d.get(frappe.scrub(dimension), 0.0) + flt(entry.debit) - flt(entry.credit)
+
+def prepare_data(accounts, filters, company_currency, dimension_list):
 	data = []
 
 	for d in accounts:
@@ -135,13 +131,13 @@
 				if d.account_number else d.account_name)
 		}
 
-		for item in dimension_items_list:
-			row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3)
+		for dimension in dimension_list:
+			row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3)
 
-			if abs(row[frappe.scrub(item)]) >= 0.005:
+			if abs(row[frappe.scrub(dimension)]) >= 0.005:
 				# ignore zero values
 				has_value = True
-				total += flt(d.get(frappe.scrub(item), 0.0), 3)
+				total += flt(d.get(frappe.scrub(dimension), 0.0), 3)
 
 		row["has_value"] = has_value
 		row["total"] = total
@@ -149,62 +145,55 @@
 
 	return data
 
-def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list):
+def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list):
 	"""accumulate children's values in parent accounts"""
 	for d in reversed(accounts):
 		if d.parent_account:
-			for item in dimension_items_list:
-				accounts_by_name[d.parent_account][frappe.scrub(item)] = \
-					accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0)
+			for dimension in dimension_list:
+				accounts_by_name[d.parent_account][frappe.scrub(dimension)] = \
+					accounts_by_name[d.parent_account].get(frappe.scrub(dimension), 0.0) + d.get(frappe.scrub(dimension), 0.0)
 
-def get_condition(from_date, item, dimension):
+def get_condition(dimension):
 	conditions = []
 
-	if from_date:
-		conditions.append("posting_date >= %(from_date)s")
-	if dimension:
-		if dimension not in ['Cost Center', 'Project']:
-			if dimension in ['Customer', 'Supplier']:
-				dimension = 'Party'
-			else:
-				dimension = 'Voucher No'
-		txt = "{0} = %(item)s".format(frappe.scrub(dimension))
-		conditions.append(txt)
+	conditions.append("{0} in %(dimensions)s".format(frappe.scrub(dimension)))
 
 	return " and {}".format(" and ".join(conditions)) if conditions else ""
 
-def get_dimension_items_list(dimension, company):
-	meta = frappe.get_meta(dimension, cached=False)
-	fieldnames = [d.fieldname for d in meta.get("fields")]
-	filters = {}
-	if 'company' in fieldnames:
-		filters['company'] = company
-	return frappe.get_all(dimension, filters, as_list=True)
+def get_dimensions(filters):
+	meta = frappe.get_meta(filters.get('dimension'), cached=False)
+	query_filters = {}
 
-def get_columns(dimension_items_list, accumulated_values=1, company=None):
+	if meta.has_field('company'):
+		query_filters = {'company': filters.get('company')}
+
+	return frappe.get_all(filters.get('dimension'), filters=query_filters, pluck='name')
+
+def get_columns(dimension_list):
 	columns = [{
 		"fieldname": "account",
 		"label": _("Account"),
 		"fieldtype": "Link",
 		"options": "Account",
 		"width": 300
+	},
+	{
+		"fieldname": "currency",
+		"label": _("Currency"),
+		"fieldtype": "Link",
+		"options": "Currency",
+		"hidden": 1
 	}]
-	if company:
+
+	for dimension in dimension_list:
 		columns.append({
-			"fieldname": "currency",
-			"label": _("Currency"),
-			"fieldtype": "Link",
-			"options": "Currency",
-			"hidden": 1
-		})
-	for item in dimension_items_list:
-		columns.append({
-			"fieldname": frappe.scrub(item),
-			"label": item,
+			"fieldname": frappe.scrub(dimension),
+			"label": dimension,
 			"fieldtype": "Currency",
 			"options": "currency",
 			"width": 150
 		})
+
 	columns.append({
 			"fieldname": "total",
 			"label": "Total",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 48cd753..2b6e696 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -1018,21 +1018,21 @@
 	required_qty = item.get("quantity")
 	# get available material by transferring to production warehouse
 	for d in locations:
-		if required_qty <=0: return
+		if required_qty <= 0:
+			return
 
 		new_dict = copy.deepcopy(item)
 		quantity = required_qty if d.get("qty") > required_qty else d.get("qty")
 
-		if required_qty > 0:
-			new_dict.update({
-				"quantity": quantity,
-				"material_request_type": "Material Transfer",
-				"uom": new_dict.get("stock_uom"),  # internal transfer should be in stock UOM
-				"from_warehouse": d.get("warehouse")
-			})
+		new_dict.update({
+			"quantity": quantity,
+			"material_request_type": "Material Transfer",
+			"uom": new_dict.get("stock_uom"),  # internal transfer should be in stock UOM
+			"from_warehouse": d.get("warehouse")
+		})
 
-			required_qty -= quantity
-			new_mr_items.append(new_dict)
+		required_qty -= quantity
+		new_mr_items.append(new_dict)
 
 	# raise purchase request for remaining qty
 	if required_qty:
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 08270bd..81ff351 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -75,7 +75,7 @@
 				fieldtype:'Float',
 				read_only: me.has_batch && !me.has_serial_no,
 				label: __(me.has_batch && !me.has_serial_no ? 'Selected Qty' : 'Qty'),
-				default: flt(me.item.stock_qty),
+				default: flt(me.item.stock_qty) || flt(me.item.transfer_qty),
 			},
 			...get_pending_qty_fields(me),
 			{
@@ -94,14 +94,16 @@
 				description: __('Fetch Serial Numbers based on FIFO'),
 				click: () => {
 					let qty = this.dialog.fields_dict.qty.get_value();
+					let already_selected_serial_nos = get_selected_serial_nos(me);
 					let numbers = frappe.call({
 						method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
 						args: {
 							qty: qty,
 							item_code: me.item_code,
 							warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
-							batch_no: me.item.batch_no || null,
-							posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date
+							batch_nos: me.item.batch_no || null,
+							posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date,
+							exclude_sr_nos: already_selected_serial_nos
 						}
 					});
 
@@ -577,15 +579,29 @@
 	return pending_qty_fields;
 }
 
-function calc_total_selected_qty(me) {
+// get all items with same item code except row for which selector is open.
+function get_rows_with_same_item_code(me) {
 	const { frm: { doc: { items }}, item: { name, item_code }} = me;
-	const totalSelectedQty = items
-		.filter( item => ( item.name !== name ) && ( item.item_code === item_code ) )
-		.map( item => flt(item.qty) )
-		.reduce( (i, j) => i + j, 0);
+	return items.filter(item => (item.name !== name) && (item.item_code === item_code))
+}
+
+function calc_total_selected_qty(me) {
+	const totalSelectedQty = get_rows_with_same_item_code(me)
+		.map(item => flt(item.qty))
+		.reduce((i, j) => i + j, 0);
 	return totalSelectedQty;
 }
 
+function get_selected_serial_nos(me) {
+	const selected_serial_nos = get_rows_with_same_item_code(me)
+		.map(item => item.serial_no)
+		.filter(serial => serial)
+		.map(sr_no_string => sr_no_string.split('\n'))
+		.reduce((acc, arr) => acc.concat(arr), [])
+		.filter(serial => serial);
+	return selected_serial_nos;
+};
+
 function check_can_calculate_pending_qty(me) {
 	const { frm: { doc }, item } = me;
 	const docChecks = doc.bom_no
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 8ede955..3abeecd 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -107,6 +107,7 @@
 		self.validate_variant_attributes()
 		self.validate_variant_based_on_change()
 		self.validate_fixed_asset()
+		self.clear_retain_sample()
 		self.validate_retain_sample()
 		self.validate_uom_conversion_factor()
 		self.validate_customer_provided_part()
@@ -209,6 +210,13 @@
 			frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(
 				self.item_code))
 
+	def clear_retain_sample(self):
+		if not self.has_batch_no:
+			self.retain_sample = None
+
+		if not self.retain_sample:
+			self.sample_quantity = None
+
 	def add_default_uom_in_conversion_factor_table(self):
 		if not self.is_new() and self.has_value_changed("stock_uom"):
 			self.uoms = []
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 669cabc..c12200b 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -656,6 +656,20 @@
 		make_stock_entry(qty=1, item_code=item.name, target="_Test Warehouse - _TC", posting_date = add_days(today(), 5))
 		self.consume_item_code_with_differet_stock_transactions(item_code=item.name)
 
+	@change_settings("Stock Settings", {"sample_retention_warehouse": 0})
+	def test_retain_sample(self):
+		frappe.db.set_single_value('Stock Settings', 'sample_retention_warehouse', '_Test Retain Sample Warehouse')
+		item = make_item("_TestRetainSample", {'has_batch_no': 1, 'retain_sample': 1, 'sample_quantity': 1})
+
+		self.assertEqual(item.has_batch_no, 1)
+		self.assertEqual(item.retain_sample, 1)
+		self.assertEqual(item.sample_quantity, 1)
+
+		item.has_batch_no = None
+		item.save()
+		self.assertEqual(item.retain_sample, None)
+		self.assertEqual(item.sample_quantity, None)
+		item.delete()
 
 	def consume_item_code_with_differet_stock_transactions(self, item_code, warehouse="_Test Warehouse - _TC"):
 		from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 51209ac..49fefae 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -82,6 +82,9 @@
 		self.reset_default_field_value("set_warehouse", "items", "warehouse")
 		self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
 
+	def before_update_after_submit(self):
+		self.validate_schedule_date()
+
 	def validate_material_request_type(self):
 		""" Validate fields in accordance with selected type """
 
diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json
index 2bad42a..dd66cff 100644
--- a/erpnext/stock/doctype/material_request_item/material_request_item.json
+++ b/erpnext/stock/doctype/material_request_item/material_request_item.json
@@ -177,6 +177,7 @@
    "fieldtype": "Column Break"
   },
   {
+   "allow_on_submit": 1,
    "bold": 1,
    "columns": 2,
    "fieldname": "schedule_date",
@@ -459,7 +460,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-11-03 14:40:24.409826",
+ "modified": "2022-03-10 18:42:42.705190",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Material Request Item",
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index bf62f50..c77c6c3 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -7,7 +7,17 @@
 import frappe
 from frappe import ValidationError, _
 from frappe.model.naming import make_autoname
-from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate
+from frappe.query_builder.functions import Coalesce
+from frappe.utils import (
+	add_days,
+	cint,
+	cstr,
+	flt,
+	get_link_to_form,
+	getdate,
+	nowdate,
+	safe_json_loads,
+)
 
 from erpnext.controllers.stock_controller import StockController
 from erpnext.stock.get_item_details import get_reserved_qty_for_so
@@ -564,26 +574,33 @@
 	return serial_nos
 
 @frappe.whitelist()
-def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch_nos=None, for_doctype=None):
-	filters = { "item_code": item_code, "warehouse": warehouse }
+def auto_fetch_serial_number(qty, item_code, warehouse,
+		posting_date=None, batch_nos=None, for_doctype=None, exclude_sr_nos=None):
+
+	filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})
+
+	if exclude_sr_nos is None:
+		exclude_sr_nos = []
+	else:
+		exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos)))
 
 	if batch_nos:
-		try:
-			filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)]
-		except Exception:
-			filters["batch_no"] = [batch_nos]
+		batch_nos = safe_json_loads(batch_nos)
+		if isinstance(batch_nos, list):
+			filters.batch_no = batch_nos
+		elif isinstance(batch_nos, str):
+			filters.batch_no = [batch_nos]
 
 	if posting_date:
-		filters["expiry_date"] = posting_date
+		filters.expiry_date = posting_date
 
 	serial_numbers = []
 	if for_doctype == 'POS Invoice':
-		reserved_sr_nos = get_pos_reserved_serial_nos(filters)
-		serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=reserved_sr_nos)
-	else:
-		serial_numbers = fetch_serial_numbers(filters, qty)
+		exclude_sr_nos.extend(get_pos_reserved_serial_nos(filters))
 
-	return [d.get('name') for d in serial_numbers]
+	serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=exclude_sr_nos)
+
+	return sorted([d.get('name') for d in serial_numbers])
 
 @frappe.whitelist()
 def get_pos_reserved_serial_nos(filters):
@@ -610,37 +627,37 @@
 def fetch_serial_numbers(filters, qty, do_not_include=None):
 	if do_not_include is None:
 		do_not_include = []
-	batch_join_selection = ""
-	batch_no_condition = ""
+
 	batch_nos = filters.get("batch_no")
 	expiry_date = filters.get("expiry_date")
+	serial_no = frappe.qb.DocType("Serial No")
+
+	query = (
+		frappe.qb
+			.from_(serial_no)
+			.select(serial_no.name)
+			.where(
+				(serial_no.item_code == filters["item_code"])
+				& (serial_no.warehouse == filters["warehouse"])
+				& (Coalesce(serial_no.sales_invoice, "") == "")
+				& (Coalesce(serial_no.delivery_document_no, "") == "")
+			)
+			.orderby(serial_no.creation)
+			.limit(qty or 1)
+	)
+
+	if do_not_include:
+		query = query.where(serial_no.name.notin(do_not_include))
+
 	if batch_nos:
-		batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join("'%s'" % d for d in batch_nos))
+		query = query.where(serial_no.batch_no.isin(batch_nos))
 
 	if expiry_date:
-		batch_join_selection = "LEFT JOIN `tabBatch` batch on sr.batch_no = batch.name "
-		expiry_date_cond = "AND ifnull(batch.expiry_date, '2500-12-31') >= %(expiry_date)s "
-		batch_no_condition += expiry_date_cond
+		batch = frappe.qb.DocType("Batch")
+		query = (query
+			.left_join(batch).on(serial_no.batch_no == batch.name)
+			.where(Coalesce(batch.expiry_date, "4000-12-31") >= expiry_date)
+		)
 
-	excluded_sr_nos = ", ".join(["" + frappe.db.escape(sr) + "" for sr in do_not_include]) or "''"
-	serial_numbers = frappe.db.sql("""
-		SELECT sr.name FROM `tabSerial No` sr {batch_join_selection}
-		WHERE
-			sr.name not in ({excluded_sr_nos}) AND
-			sr.item_code = %(item_code)s AND
-			sr.warehouse = %(warehouse)s AND
-			ifnull(sr.sales_invoice,'') = '' AND
-			ifnull(sr.delivery_document_no, '') = ''
-			{batch_no_condition}
-		ORDER BY
-			sr.creation
-		LIMIT
-			{qty}
-		""".format(
-				excluded_sr_nos=excluded_sr_nos,
-				qty=qty or 1,
-				batch_join_selection=batch_join_selection,
-				batch_no_condition=batch_no_condition
-			), filters, as_dict=1)
-
+	serial_numbers = query.run(as_dict=True)
 	return serial_numbers
diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py
index 057a7d4..cca6307 100644
--- a/erpnext/stock/doctype/serial_no/test_serial_no.py
+++ b/erpnext/stock/doctype/serial_no/test_serial_no.py
@@ -6,10 +6,12 @@
 
 
 import frappe
+from frappe.tests.utils import FrappeTestCase
 
 from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
 from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+from erpnext.stock.doctype.serial_no.serial_no import *
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
 from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
 from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
@@ -18,9 +20,6 @@
 test_dependencies = ["Item"]
 test_records = frappe.get_test_records('Serial No')
 
-from frappe.tests.utils import FrappeTestCase
-
-from erpnext.stock.doctype.serial_no.serial_no import *
 
 
 class TestSerialNo(FrappeTestCase):
@@ -242,3 +241,56 @@
 			)
 		self.assertEqual(value_diff, -113)
 
+	def test_auto_fetch(self):
+		item_code = make_item(properties={
+			"has_serial_no": 1,
+			"has_batch_no": 1,
+			"create_new_batch": 1,
+			"serial_no_series": "TEST.#######"
+		}).name
+		warehouse = "_Test Warehouse - _TC"
+
+		in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5)
+		in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5)
+
+		in1.reload()
+		in2.reload()
+
+		batch1 = in1.items[0].batch_no
+		batch2 = in2.items[0].batch_no
+
+		batch_wise_serials = {
+			batch1 : get_serial_nos(in1.items[0].serial_no),
+			batch2: get_serial_nos(in2.items[0].serial_no)
+		}
+
+		# Test FIFO
+		first_fetch = auto_fetch_serial_number(5, item_code, warehouse)
+		self.assertEqual(first_fetch, batch_wise_serials[batch1])
+
+		# partial FIFO
+		partial_fetch = auto_fetch_serial_number(2, item_code, warehouse)
+		self.assertTrue(set(partial_fetch).issubset(set(first_fetch)),
+				msg=f"{partial_fetch} should be subset of {first_fetch}")
+
+		# exclusion
+		remaining = auto_fetch_serial_number(3, item_code, warehouse, exclude_sr_nos=partial_fetch)
+		self.assertEqual(sorted(remaining + partial_fetch), first_fetch)
+
+		# batchwise
+		for batch, expected_serials in batch_wise_serials.items():
+			fetched_sr = auto_fetch_serial_number(5, item_code, warehouse, batch_nos=batch)
+			self.assertEqual(fetched_sr, sorted(expected_serials))
+
+		# non existing warehouse
+		self.assertEqual(auto_fetch_serial_number(10, item_code, warehouse="Nonexisting"), [])
+
+		# multi batch
+		all_serials = [sr for sr_list in batch_wise_serials.values() for sr in sr_list]
+		fetched_serials = auto_fetch_serial_number(10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys()))
+		self.assertEqual(sorted(all_serials), fetched_serials)
+
+		# expiry date
+		frappe.db.set_value("Batch", batch1, "expiry_date", "1980-01-01")
+		non_expired_serials = auto_fetch_serial_number(5, item_code, warehouse, posting_date="2021-01-01", batch_nos=batch1)
+		self.assertEqual(non_expired_serials, [])
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 c538307..2f59304 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -26,6 +26,8 @@
 		name will be changed using autoname options (in a scheduled job)
 		"""
 		self.name = frappe.generate_hash(txt="", length=10)
+		if self.meta.autoname == "hash":
+			self.to_rename = 0
 
 	def validate(self):
 		self.flags.ignore_submit_comment = True
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 2352235..fc57995 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -7,9 +7,11 @@
 
 import frappe
 from frappe.core.page.permission_manager.permission_manager import reset
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter
 from frappe.tests.utils import FrappeTestCase
 from frappe.utils import add_days, today
 
+from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
 from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
 from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
@@ -939,3 +941,62 @@
 	# Used to isolate valuation sensitive
 	# tests to prevent future tests from failing.
 	return str(uuid4())[:8].upper()
+
+
+class TestDeferredNaming(FrappeTestCase):
+
+	@classmethod
+	def setUpClass(cls) -> None:
+		super().setUpClass()
+		cls.gle_autoname = frappe.get_meta("GL Entry").autoname
+		cls.sle_autoname = frappe.get_meta("Stock Ledger Entry").autoname
+
+	def setUp(self) -> None:
+		self.item = make_item().name
+		self.warehouse = "Stores - TCP1"
+		self.company = "_Test Company with perpetual inventory"
+
+	def tearDown(self) -> None:
+		make_property_setter(doctype="GL Entry", for_doctype=True,
+				property="autoname", value=self.gle_autoname, property_type="Data", fieldname=None)
+		make_property_setter(doctype="Stock Ledger Entry", for_doctype=True,
+				property="autoname", value=self.sle_autoname, property_type="Data", fieldname=None)
+
+		# since deferred naming autocommits, commit all changes to avoid flake
+		frappe.db.commit()  # nosemgrep
+
+	@staticmethod
+	def get_gle_sles(se):
+		filters = {"voucher_type": se.doctype, "voucher_no": se.name}
+		gle = set(frappe.get_list("GL Entry", filters, pluck="name"))
+		sle = set(frappe.get_list("Stock Ledger Entry", filters, pluck="name"))
+		return gle, sle
+
+	def test_deferred_naming(self):
+		se = make_stock_entry(item_code=self.item, to_warehouse=self.warehouse,
+				qty=10, rate=100, company=self.company)
+
+		gle, sle = self.get_gle_sles(se)
+		rename_gle_sle_docs()
+		renamed_gle, renamed_sle  = self.get_gle_sles(se)
+
+		self.assertFalse(gle & renamed_gle, msg="GLEs not renamed")
+		self.assertFalse(sle & renamed_sle, msg="SLEs not renamed")
+		se.cancel()
+
+	def test_hash_naming(self):
+		# disable naming series
+		for doctype in ("GL Entry", "Stock Ledger Entry"):
+			make_property_setter(doctype=doctype, for_doctype=True,
+					property="autoname", value="hash", property_type="Data", fieldname=None)
+
+		se = make_stock_entry(item_code=self.item, to_warehouse=self.warehouse,
+				qty=10, rate=100, company=self.company)
+
+		gle, sle = self.get_gle_sles(se)
+		rename_gle_sle_docs()
+		renamed_gle, renamed_sle  = self.get_gle_sles(se)
+
+		self.assertEqual(gle, renamed_gle, msg="GLEs are renamed while using hash naming")
+		self.assertEqual(sle, renamed_sle, msg="SLEs are renamed while using hash naming")
+		se.cancel()
diff --git a/erpnext/stock/doctype/warehouse/test_records.json b/erpnext/stock/doctype/warehouse/test_records.json
index e128558..1b1e905 100644
--- a/erpnext/stock/doctype/warehouse/test_records.json
+++ b/erpnext/stock/doctype/warehouse/test_records.json
@@ -60,5 +60,12 @@
   "warehouse_name": "_Test Warehouse Group-C2",
   "is_group": 0,
   "parent_warehouse": "_Test Warehouse Group - _TC"
+ },
+  {
+  "company": "_Test Company",
+  "doctype": "Warehouse",
+  "warehouse_name": "_Test Retain Sample Warehouse",
+  "parent_warehouse": "_Test Warehouse Group - _TC",
+  "is_group": 0
  }
 ]
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index cdb7719..08d7c99 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -4,12 +4,12 @@
 import frappe
 from frappe.test_runner import make_test_records
 from frappe.tests.utils import FrappeTestCase
-from frappe.utils import cint
 
 import erpnext
-from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
+from erpnext.accounts.doctype.account.test_account import create_account
 from erpnext.stock.doctype.item.test_item import create_item
 from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.stock.doctype.warehouse.warehouse import convert_to_group_or_ledger, get_children
 
 test_records = frappe.get_test_records('Warehouse')
 
@@ -65,6 +65,33 @@
 					f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}."
 				)
 
+	def test_group_non_group_conversion(self):
+
+		warehouse = frappe.get_doc("Warehouse", create_warehouse("TestGroupConversion"))
+
+		convert_to_group_or_ledger(warehouse.name)
+		warehouse.reload()
+		self.assertEqual(warehouse.is_group, 1)
+
+		child = create_warehouse("GroupWHChild", {"parent_warehouse": warehouse.name})
+		# chid exists
+		self.assertRaises(frappe.ValidationError, convert_to_group_or_ledger, warehouse.name)
+		frappe.delete_doc("Warehouse", child)
+
+		convert_to_group_or_ledger(warehouse.name)
+		warehouse.reload()
+		self.assertEqual(warehouse.is_group, 0)
+
+		make_stock_entry(item_code="_Test Item", target=warehouse.name, qty=1)
+		# SLE exists
+		self.assertRaises(frappe.ValidationError, convert_to_group_or_ledger, warehouse.name)
+
+	def test_get_children(self):
+		company = "_Test Company"
+
+		children = get_children("Warehouse", parent=company, company=company, is_root=True)
+		self.assertTrue(any(wh['value'] == "_Test Warehouse - _TC" for wh in children))
+
 
 def create_warehouse(warehouse_name, properties=None, company=None):
 	if not company:
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index 9cfad86..4c7f41d 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -41,14 +41,11 @@
 
 	def on_trash(self):
 		# delete bin
-		bins = frappe.db.sql("select * from `tabBin` where warehouse = %s",
-			self.name, as_dict=1)
+		bins = frappe.get_all("Bin", fields="*", filters={"warehouse": self.name})
 		for d in bins:
 			if d['actual_qty'] or d['reserved_qty'] or d['ordered_qty'] or \
 					d['indented_qty'] or d['projected_qty'] or d['planned_qty']:
 				throw(_("Warehouse {0} can not be deleted as quantity exists for Item {1}").format(self.name, d['item_code']))
-			else:
-				frappe.db.sql("delete from `tabBin` where name = %s", d['name'])
 
 		if self.check_if_sle_exists():
 			throw(_("Warehouse can not be deleted as stock ledger entry exists for this warehouse."))
@@ -56,16 +53,15 @@
 		if self.check_if_child_exists():
 			throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse."))
 
+		frappe.db.delete("Bin", filters={"warehouse": self.name})
 		self.update_nsm_model()
 		self.unlink_from_items()
 
 	def check_if_sle_exists(self):
-		return frappe.db.sql("""select name from `tabStock Ledger Entry`
-			where warehouse = %s limit 1""", self.name)
+		return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name})
 
 	def check_if_child_exists(self):
-		return frappe.db.sql("""select name from `tabWarehouse`
-			where parent_warehouse = %s limit 1""", self.name)
+		return frappe.db.exists("Warehouse", {"parent_warehouse": self.name})
 
 	def convert_to_group_or_ledger(self):
 		if self.is_group:
@@ -92,10 +88,7 @@
 			return 1
 
 	def unlink_from_items(self):
-		frappe.db.sql("""
-				update `tabItem Default`
-				set default_warehouse=NULL
-				where default_warehouse=%s""", self.name)
+		frappe.db.set_value("Item Default", {"default_warehouse": self.name}, "default_warehouse", None)
 
 @frappe.whitelist()
 def get_children(doctype, parent=None, company=None, is_root=False):
@@ -164,15 +157,16 @@
 	frappe.get_doc(args).insert()
 
 @frappe.whitelist()
-def convert_to_group_or_ledger():
-	args = frappe.form_dict
-	return frappe.get_doc("Warehouse", args.docname).convert_to_group_or_ledger()
+def convert_to_group_or_ledger(docname=None):
+	if not docname:
+		docname = frappe.form_dict.docname
+	return frappe.get_doc("Warehouse", docname).convert_to_group_or_ledger()
 
 def get_child_warehouses(warehouse):
-	lft, rgt = frappe.get_cached_value("Warehouse", warehouse, ["lft", "rgt"])
+	from frappe.utils.nestedset import get_descendants_of
 
-	return frappe.db.sql_list("""select name from `tabWarehouse`
-		where lft >= %s and rgt <= %s""", (lft, rgt))
+	children = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft")
+	return children + [warehouse]   # append self for backward compatibility
 
 def get_warehouses_based_on_account(account, company=None):
 	warehouses = []
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index 97a740e..7ca4003 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -12,7 +12,7 @@
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
 
 Filters = frappe._dict
-precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
+
 
 def execute(filters: Filters = None) -> Tuple:
 	to_date = filters["to_date"]
@@ -30,6 +30,8 @@
 	_func = itemgetter(1)
 	data = []
 
+	precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
+
 	for item, item_dict in item_details.items():
 		earliest_age, latest_age = 0, 0
 		details = item_dict["details"]
@@ -76,6 +78,9 @@
 	return flt(age_qty / total_qty, 2) if total_qty else 0.0
 
 def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple:
+
+	precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
+
 	range1 = range2 = range3 = above_range3 = 0.0
 
 	for item in fifo_queue: