Merge pull request #35567 from s-aga-r/FIX-SBB-STATUS

fix(ux): serial and batch bundle status
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index ce1ed33..81ff6a5 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -50,13 +50,15 @@
 		if frappe.flags.in_test:
 			make_dimension_in_accounting_doctypes(doc=self)
 		else:
-			frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long")
+			frappe.enqueue(
+				make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
+			)
 
 	def on_trash(self):
 		if frappe.flags.in_test:
 			delete_accounting_dimension(doc=self)
 		else:
-			frappe.enqueue(delete_accounting_dimension, doc=self, queue="long")
+			frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True)
 
 	def set_fieldname_and_label(self):
 		if not self.label:
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
index 9d636ad..641f452 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -44,6 +44,7 @@
 				voucher_type="Period Closing Voucher",
 				voucher_no=self.name,
 				queue="long",
+				enqueue_after_commit=True,
 			)
 			frappe.msgprint(
 				_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index f86dd8f..e606308 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -647,12 +647,12 @@
 	else:
 		args.update(get_party_details(party, party_type))
 
-	if party_type in ("Customer", "Lead"):
+	if party_type in ("Customer", "Lead", "Prospect"):
 		args.update({"tax_type": "Sales"})
 
-		if party_type == "Lead":
+		if party_type in ["Lead", "Prospect"]:
 			args["customer"] = None
-			del args["lead"]
+			del args[frappe.scrub(party_type)]
 	else:
 		args.update({"tax_type": "Purchase"})
 
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 707db8a..2e290e3 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -758,6 +758,7 @@
 			}
 		)
 
+		update_gl_dict_with_regional_fields(self, gl_dict)
 		accounting_dimensions = get_accounting_dimensions()
 		dimension_dict = frappe._dict()
 
@@ -2846,3 +2847,8 @@
 @erpnext.allow_regional
 def validate_einvoice_fields(doc):
 	pass
+
+
+@erpnext.allow_regional
+def update_gl_dict_with_regional_fields(doc, gl_dict):
+	pass
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index f1cef71..3bb1128 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -3,12 +3,13 @@
 
 
 import json
-from collections import defaultdict
+from collections import OrderedDict, defaultdict
 
 import frappe
 from frappe import scrub
 from frappe.desk.reportview import get_filters_cond, get_match_cond
-from frappe.utils import nowdate, unique
+from frappe.query_builder.functions import Concat, Sum
+from frappe.utils import nowdate, today, unique
 
 import erpnext
 from erpnext.stock.get_item_details import _get_item_tax_template
@@ -412,95 +413,136 @@
 @frappe.validate_and_sanitize_search_inputs
 def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
 	doctype = "Batch"
-	cond = ""
-	if filters.get("posting_date"):
-		cond = "and (batch.expiry_date is null or batch.expiry_date >= %(posting_date)s)"
-
-	batch_nos = None
-	args = {
-		"item_code": filters.get("item_code"),
-		"warehouse": filters.get("warehouse"),
-		"posting_date": filters.get("posting_date"),
-		"txt": "%{0}%".format(txt),
-		"start": start,
-		"page_len": page_len,
-	}
-
-	having_clause = "having sum(sle.actual_qty) > 0"
-	if filters.get("is_return"):
-		having_clause = ""
-
 	meta = frappe.get_meta(doctype, cached=True)
 	searchfields = meta.get_search_fields()
 
-	search_columns = ""
-	search_cond = ""
+	query = get_batches_from_stock_ledger_entries(searchfields, txt, filters)
+	bundle_query = get_batches_from_serial_and_batch_bundle(searchfields, txt, filters)
 
-	if searchfields:
-		search_columns = ", " + ", ".join(searchfields)
-		search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields])
+	data = (
+		frappe.qb.from_((query) + (bundle_query))
+		.select("batch_no", "qty", "manufacturing_date", "expiry_date")
+		.offset(start)
+		.limit(page_len)
+	)
 
-	if args.get("warehouse"):
-		searchfields = ["batch." + field for field in searchfields]
-		if searchfields:
-			search_columns = ", " + ", ".join(searchfields)
-			search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields])
+	for field in searchfields:
+		data = data.select(field)
 
-		batch_nos = frappe.db.sql(
-			"""select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom,
-				concat('MFG-',batch.manufacturing_date), concat('EXP-',batch.expiry_date)
-				{search_columns}
-			from `tabStock Ledger Entry` sle
-				INNER JOIN `tabBatch` batch on sle.batch_no = batch.name
-			where
-				batch.disabled = 0
-				and sle.is_cancelled = 0
-				and sle.item_code = %(item_code)s
-				and sle.warehouse = %(warehouse)s
-				and (sle.batch_no like %(txt)s
-				or batch.expiry_date like %(txt)s
-				or batch.manufacturing_date like %(txt)s
-				{search_cond})
-				and batch.docstatus < 2
-				{cond}
-				{match_conditions}
-			group by batch_no {having_clause}
-			order by batch.expiry_date, sle.batch_no desc
-			limit %(page_len)s offset %(start)s""".format(
-				search_columns=search_columns,
-				cond=cond,
-				match_conditions=get_match_cond(doctype),
-				having_clause=having_clause,
-				search_cond=search_cond,
-			),
-			args,
+	data = data.run()
+	data = get_filterd_batches(data)
+
+	return data
+
+
+def get_filterd_batches(data):
+	batches = OrderedDict()
+
+	for batch_data in data:
+		if batch_data[0] not in batches:
+			batches[batch_data[0]] = list(batch_data)
+		else:
+			batches[batch_data[0]][1] += batch_data[1]
+
+	filterd_batch = []
+	for batch, batch_data in batches.items():
+		if batch_data[1] > 0:
+			filterd_batch.append(tuple(batch_data))
+
+	return filterd_batch
+
+
+def get_batches_from_stock_ledger_entries(searchfields, txt, filters):
+	stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
+	batch_table = frappe.qb.DocType("Batch")
+
+	expiry_date = filters.get("posting_date") or today()
+
+	query = (
+		frappe.qb.from_(stock_ledger_entry)
+		.inner_join(batch_table)
+		.on(batch_table.name == stock_ledger_entry.batch_no)
+		.select(
+			stock_ledger_entry.batch_no,
+			Sum(stock_ledger_entry.actual_qty).as_("qty"),
 		)
-
-		return batch_nos
-	else:
-		return frappe.db.sql(
-			"""select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date)
-			{search_columns}
-			from `tabBatch` batch
-			where batch.disabled = 0
-			and item = %(item_code)s
-			and (name like %(txt)s
-			or expiry_date like %(txt)s
-			or manufacturing_date like %(txt)s
-			{search_cond})
-			and docstatus < 2
-			{0}
-			{match_conditions}
-
-			order by expiry_date, name desc
-			limit %(page_len)s offset %(start)s""".format(
-				cond,
-				search_columns=search_columns,
-				search_cond=search_cond,
-				match_conditions=get_match_cond(doctype),
-			),
-			args,
+		.where(((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())))
+		.where(stock_ledger_entry.is_cancelled == 0)
+		.where(
+			(stock_ledger_entry.item_code == filters.get("item_code"))
+			& (batch_table.disabled == 0)
+			& (stock_ledger_entry.batch_no.isnotnull())
 		)
+		.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
+	)
+
+	query = query.select(
+		Concat("MFG-", batch_table.manufacturing_date).as_("manufacturing_date"),
+		Concat("EXP-", batch_table.expiry_date).as_("expiry_date"),
+	)
+
+	if filters.get("warehouse"):
+		query = query.where(stock_ledger_entry.warehouse == filters.get("warehouse"))
+
+	for field in searchfields:
+		query = query.select(batch_table[field])
+
+	if txt:
+		txt_condition = batch_table.name.like(txt)
+		for field in searchfields + ["name"]:
+			txt_condition |= batch_table[field].like(txt)
+
+		query = query.where(txt_condition)
+
+	return query
+
+
+def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters):
+	bundle = frappe.qb.DocType("Serial and Batch Entry")
+	stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
+	batch_table = frappe.qb.DocType("Batch")
+
+	expiry_date = filters.get("posting_date") or today()
+
+	bundle_query = (
+		frappe.qb.from_(bundle)
+		.inner_join(stock_ledger_entry)
+		.on(bundle.parent == stock_ledger_entry.serial_and_batch_bundle)
+		.inner_join(batch_table)
+		.on(batch_table.name == bundle.batch_no)
+		.select(
+			bundle.batch_no,
+			Sum(bundle.qty).as_("qty"),
+		)
+		.where(((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())))
+		.where(stock_ledger_entry.is_cancelled == 0)
+		.where(
+			(stock_ledger_entry.item_code == filters.get("item_code"))
+			& (batch_table.disabled == 0)
+			& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
+		)
+		.groupby(bundle.batch_no, bundle.warehouse)
+	)
+
+	bundle_query = bundle_query.select(
+		Concat("MFG-", batch_table.manufacturing_date),
+		Concat("EXP-", batch_table.expiry_date),
+	)
+
+	if filters.get("warehouse"):
+		bundle_query = bundle_query.where(stock_ledger_entry.warehouse == filters.get("warehouse"))
+
+	for field in searchfields:
+		bundle_query = bundle_query.select(batch_table[field])
+
+	if txt:
+		txt_condition = batch_table.name.like(txt)
+		for field in searchfields + ["name"]:
+			txt_condition |= batch_table[field].like(txt)
+
+		bundle_query = bundle_query.where(txt_condition)
+
+	return bundle_query
 
 
 @frappe.whitelist()
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index d319533..6f1a50d 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -43,7 +43,6 @@
 				self.set_serial_and_batch_bundle(table_field)
 
 	def set_missing_values(self, for_validate=False):
-
 		super(SellingController, self).set_missing_values(for_validate)
 
 		# set contact and address details for customer, if they are not mentioned
@@ -62,7 +61,7 @@
 		elif self.doctype == "Quotation" and self.party_name:
 			if self.quotation_to == "Customer":
 				customer = self.party_name
-			else:
+			elif self.quotation_to == "Lead":
 				lead = self.party_name
 
 		if customer:
diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
index 4daa2ed..9cc6ec9 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
@@ -160,4 +160,3 @@
 		interest = per_day_interest * 15
 
 		self.assertEqual(amounts["pending_principal_amount"], 1500000)
-		self.assertEqual(amounts["interest_amount"], flt(interest + previous_interest, 2))
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
index cac3f1f..ced6394 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -22,7 +22,7 @@
 			frappe.throw(_("Interest Amount or Principal Amount is mandatory"))
 
 		if not self.last_accrual_date:
-			self.last_accrual_date = get_last_accrual_date(self.loan)
+			self.last_accrual_date = get_last_accrual_date(self.loan, self.posting_date)
 
 	def on_submit(self):
 		self.make_gl_entries()
@@ -274,14 +274,14 @@
 
 
 def get_no_of_days_for_interest_accural(loan, posting_date):
-	last_interest_accrual_date = get_last_accrual_date(loan.name)
+	last_interest_accrual_date = get_last_accrual_date(loan.name, posting_date)
 
 	no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1
 
 	return no_of_days
 
 
-def get_last_accrual_date(loan):
+def get_last_accrual_date(loan, posting_date):
 	last_posting_date = frappe.db.sql(
 		""" SELECT MAX(posting_date) from `tabLoan Interest Accrual`
 		WHERE loan = %s and docstatus = 1""",
@@ -289,12 +289,30 @@
 	)
 
 	if last_posting_date[0][0]:
+		last_interest_accrual_date = last_posting_date[0][0]
 		# interest for last interest accrual date is already booked, so add 1 day
-		return add_days(last_posting_date[0][0], 1)
+		last_disbursement_date = get_last_disbursement_date(loan, posting_date)
+
+		if last_disbursement_date and getdate(last_disbursement_date) > getdate(
+			last_interest_accrual_date
+		):
+			last_interest_accrual_date = last_disbursement_date
+
+		return add_days(last_interest_accrual_date, 1)
 	else:
 		return frappe.db.get_value("Loan", loan, "disbursement_date")
 
 
+def get_last_disbursement_date(loan, posting_date):
+	last_disbursement_date = frappe.db.get_value(
+		"Loan Disbursement",
+		{"docstatus": 1, "against_loan": loan, "posting_date": ("<", posting_date)},
+		"MAX(posting_date)",
+	)
+
+	return last_disbursement_date
+
+
 def days_in_year(year):
 	days = 365
 
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 8a185f8..82aab4a 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -101,7 +101,7 @@
 		if flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision):
 			if not self.is_term_loan:
 				# get last loan interest accrual date
-				last_accrual_date = get_last_accrual_date(self.against_loan)
+				last_accrual_date = get_last_accrual_date(self.against_loan, self.posting_date)
 
 				# get posting date upto which interest has to be accrued
 				per_day_interest = get_per_day_interest(
@@ -725,7 +725,7 @@
 	if due_date:
 		pending_days = date_diff(posting_date, due_date) + 1
 	else:
-		last_accrual_date = get_last_accrual_date(against_loan_doc.name)
+		last_accrual_date = get_last_accrual_date(against_loan_doc.name, posting_date)
 		pending_days = date_diff(posting_date, last_accrual_date) + 1
 
 	if pending_days > 0:
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
index 7477f95..17b5aae 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
@@ -88,12 +88,14 @@
 				boms=boms,
 				timeout=40000,
 				now=frappe.flags.in_test,
+				enqueue_after_commit=True,
 			)
 		else:
 			frappe.enqueue(
 				method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
 				update_doc=self,
 				now=frappe.flags.in_test,
+				enqueue_after_commit=True,
 			)
 
 
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 87a6de0..c001b4e 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -363,10 +363,16 @@
 						new erpnext.SerialBatchPackageSelector(
 							me.frm, item, (r) => {
 								if (r) {
-									frappe.model.set_value(item.doctype, item.name, {
+									let update_values = {
 										"serial_and_batch_bundle": r.name,
 										"qty": Math.abs(r.total_qty)
-									});
+									}
+
+									if (r.warehouse) {
+										update_values["warehouse"] = r.warehouse;
+									}
+
+									frappe.model.set_value(item.doctype, item.name, update_values);
 								}
 							}
 						);
@@ -392,10 +398,16 @@
 						new erpnext.SerialBatchPackageSelector(
 							me.frm, item, (r) => {
 								if (r) {
-									frappe.model.set_value(item.doctype, item.name, {
-										"rejected_serial_and_batch_bundle": r.name,
+									let update_values = {
+										"serial_and_batch_bundle": r.name,
 										"rejected_qty": Math.abs(r.total_qty)
-									});
+									}
+
+									if (r.warehouse) {
+										update_values["rejected_warehouse"] = r.warehouse;
+									}
+
+									frappe.model.set_value(item.doctype, item.name, update_values);
 								}
 							}
 						);
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 2c8e50c..a47d131 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -2292,8 +2292,9 @@
 };
 
 erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close, show_dialog) {
-	debugger
 	let warehouse, receiving_stock, existing_stock;
+
+	let warehouse_field = "warehouse";
 	if (frm.doc.is_return) {
 		if (["Purchase Receipt", "Purchase Invoice"].includes(frm.doc.doctype)) {
 			existing_stock = true;
@@ -2309,6 +2310,19 @@
 				existing_stock = true;
 				warehouse = item_row.s_warehouse;
 			}
+
+			if (in_list([
+					"Material Transfer",
+					"Send to Subcontractor",
+					"Material Issue",
+					"Material Consumption for Manufacture",
+					"Material Transfer for Manufacture"
+				], frm.doc.purpose)
+			) {
+				warehouse_field = "s_warehouse";
+			} else {
+				warehouse_field = "t_warehouse";
+			}
 		} else {
 			existing_stock = true;
 			warehouse = item_row.warehouse;
@@ -2335,10 +2349,16 @@
 
 		new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
 			if (r) {
-				frappe.model.set_value(item_row.doctype, item_row.name, {
+				let update_values = {
 					"serial_and_batch_bundle": r.name,
 					"qty": Math.abs(r.total_qty)
-				});
+				}
+
+				if (r.warehouse) {
+					update_values[warehouse_field] = r.warehouse;
+				}
+
+				frappe.model.set_value(item_row.doctype, item_row.name, update_values);
 			}
 		});
 	});
diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js
index 644adff..5c41aa0 100644
--- a/erpnext/public/js/utils/party.js
+++ b/erpnext/public/js/utils/party.js
@@ -16,8 +16,8 @@
 			|| (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) {
 
 			let party_type = "Customer";
-			if (frm.doc.quotation_to && frm.doc.quotation_to === "Lead") {
-				party_type = "Lead";
+			if (frm.doc.quotation_to && in_list(["Lead", "Prospect"], frm.doc.quotation_to)) {
+				party_type = frm.doc.quotation_to;
 			}
 
 			args = {
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 217f568..f9eec2a 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -48,6 +48,50 @@
 	get_dialog_fields() {
 		let fields = [];
 
+		fields.push({
+			fieldtype: 'Link',
+			fieldname: 'warehouse',
+			label: __('Warehouse'),
+			options: 'Warehouse',
+			default: this.get_warehouse(),
+			onchange: () => {
+				this.item.warehouse = this.dialog.get_value('warehouse');
+				this.get_auto_data()
+			},
+			get_query: () => {
+				return {
+					filters: {
+						'is_group': 0,
+						'company': this.frm.doc.company,
+					}
+				};
+			}
+		});
+
+		if (this.frm.doc.doctype === 'Stock Entry'
+			&& this.frm.doc.purpose === 'Manufacture') {
+			fields.push({
+				fieldtype: 'Column Break',
+			});
+
+			fields.push({
+				fieldtype: 'Link',
+				fieldname: 'work_order',
+				label: __('For Work Order'),
+				options: 'Work Order',
+				read_only: 1,
+				default: this.frm.doc.work_order,
+			});
+
+			fields.push({
+				fieldtype: 'Section Break',
+			});
+		}
+
+		fields.push({
+			fieldtype: 'Column Break',
+		});
+
 		if (this.item.has_serial_no) {
 			fields.push({
 				fieldtype: 'Data',
@@ -73,33 +117,10 @@
 				fieldtype: 'Data',
 				fieldname: 'scan_batch_no',
 				label: __('Scan Batch No'),
-				get_query: () => {
-					return {
-						filters: {
-							'item': this.item.item_code
-						}
-					};
-				},
 				onchange: () => this.update_serial_batch_no()
 			});
 		}
 
-		if (this.frm.doc.doctype === 'Stock Entry'
-			&& this.frm.doc.purpose === 'Manufacture') {
-			fields.push({
-				fieldtype: 'Column Break',
-			});
-
-			fields.push({
-				fieldtype: 'Link',
-				fieldname: 'work_order',
-				label: __('For Work Order'),
-				options: 'Work Order',
-				read_only: 1,
-				default: this.frm.doc.work_order,
-			});
-		}
-
 		if (this.item?.outward) {
 			fields = [...this.get_filter_fields(), ...fields];
 		} else {
@@ -246,11 +267,21 @@
 					label: __('Batch No'),
 					in_list_view: 1,
 					get_query: () => {
-						return {
-							filters: {
-								'item': this.item.item_code
+						if (!this.item.outward) {
+							return {
+								filters: {
+									'item': this.item.item_code,
+								}
 							}
-						};
+						} else {
+							return {
+								query : "erpnext.controllers.queries.get_batch_no",
+								filters: {
+									'item_code': this.item.item_code,
+									'warehouse': this.get_warehouse()
+								}
+							}
+						}
 					},
 				}
 			]
@@ -278,29 +309,31 @@
 	}
 
 	get_auto_data() {
-		const { qty, based_on } = this.dialog.get_values();
+		let { qty, based_on } = this.dialog.get_values();
 
 		if (!based_on) {
 			based_on = 'FIFO';
 		}
 
-		frappe.call({
-			method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_auto_data',
-			args: {
-				item_code: this.item.item_code,
-				warehouse: this.item.warehouse || this.item.s_warehouse,
-				has_serial_no: this.item.has_serial_no,
-				has_batch_no: this.item.has_batch_no,
-				qty: qty,
-				based_on: based_on
-			},
-			callback: (r) => {
-				if (r.message) {
-					this.dialog.fields_dict.entries.df.data = r.message;
-					this.dialog.fields_dict.entries.grid.refresh();
+		if (qty) {
+			frappe.call({
+				method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_auto_data',
+				args: {
+					item_code: this.item.item_code,
+					warehouse: this.item.warehouse || this.item.s_warehouse,
+					has_serial_no: this.item.has_serial_no,
+					has_batch_no: this.item.has_batch_no,
+					qty: qty,
+					based_on: based_on
+				},
+				callback: (r) => {
+					if (r.message) {
+						this.dialog.fields_dict.entries.df.data = r.message;
+						this.dialog.fields_dict.entries.grid.refresh();
+					}
 				}
-			}
-		});
+			});
+		}
 	}
 
 	update_serial_batch_no() {
@@ -325,6 +358,7 @@
 
 	update_ledgers() {
 		let entries = this.dialog.get_values().entries;
+		let warehouse = this.dialog.get_value('warehouse');
 
 		if (entries && !entries.length || !entries) {
 			frappe.throw(__('Please add atleast one Serial No / Batch No'));
@@ -336,6 +370,7 @@
 				entries: entries,
 				child_row: this.item,
 				doc: this.frm.doc,
+				warehouse: warehouse,
 			}
 		}).then(r => {
 			this.callback && this.callback(r.message);
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index 83fa472..2d5c3fa 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -13,7 +13,7 @@
 		frm.set_query("quotation_to", function() {
 			return{
 				"filters": {
-					"name": ["in", ["Customer", "Lead"]],
+					"name": ["in", ["Customer", "Lead", "Prospect"]],
 				}
 			}
 		});
@@ -160,19 +160,16 @@
 	}
 
 	set_dynamic_field_label(){
-		if (this.frm.doc.quotation_to == "Customer")
-		{
+		if (this.frm.doc.quotation_to == "Customer") {
 			this.frm.set_df_property("party_name", "label", "Customer");
 			this.frm.fields_dict.party_name.get_query = null;
-		}
-
-		if (this.frm.doc.quotation_to == "Lead")
-		{
+		} else if (this.frm.doc.quotation_to == "Lead") {
 			this.frm.set_df_property("party_name", "label", "Lead");
-
 			this.frm.fields_dict.party_name.get_query = function() {
 				return{	query: "erpnext.controllers.queries.lead_query" }
 			}
+		} else if (this.frm.doc.quotation_to == "Prospect") {
+			this.frm.set_df_property("party_name", "label", "Prospect");
 		}
 	}
 
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index e58bc73..6459def 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1772,7 +1772,14 @@
 		self.assertEqual(pe.references[1].reference_name, so.name)
 		self.assertEqual(pe.references[1].allocated_amount, 300)
 
-	@change_settings("Stock Settings", {"enable_stock_reservation": 1})
+	@change_settings(
+		"Stock Settings",
+		{
+			"enable_stock_reservation": 1,
+			"auto_create_serial_and_batch_bundle_for_outward": 1,
+			"pick_serial_and_batch_based_on": "FIFO",
+		},
+	)
 	def test_stock_reservation_against_sales_order(self) -> None:
 		from random import randint, uniform
 
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index cf9600e..28e5e5c 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -25,6 +25,7 @@
 	create_default_success_action()
 	create_default_energy_point_rules()
 	create_incoterms()
+	create_default_role_profiles()
 	add_company_to_session_defaults()
 	add_standard_navbar_items()
 	add_app_name()
@@ -202,3 +203,16 @@
 def hide_workspaces():
 	for ws in ["Integration", "Settings"]:
 		frappe.db.set_value("Workspace", ws, "public", 0)
+
+
+def create_default_role_profiles():
+	for module in ["Accounts", "Stock", "Manufacturing"]:
+		create_role_profile(module)
+
+
+def create_role_profile(module):
+	role_profile = frappe.new_doc("Role Profile")
+	role_profile.role_profile = _("{0} User").format(module)
+	role_profile.append("roles", {"role": module + " User"})
+	role_profile.append("roles", {"role": module + " Manager"})
+	role_profile.insert()
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 3cc59be..f91a991 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -714,6 +714,7 @@
 						template=self,
 						now=frappe.flags.in_test,
 						timeout=600,
+						enqueue_after_commit=True,
 					)
 
 	def validate_has_variants(self):
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index f463751..7e5cac9 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -916,7 +916,7 @@
 
 
 @frappe.whitelist()
-def add_serial_batch_ledgers(entries, child_row, doc) -> object:
+def add_serial_batch_ledgers(entries, child_row, doc, warehouse) -> object:
 	if isinstance(child_row, str):
 		child_row = frappe._dict(parse_json(child_row))
 
@@ -927,21 +927,23 @@
 		parent_doc = parse_json(doc)
 
 	if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle):
-		doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc)
+		doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse)
 	else:
-		doc = create_serial_batch_no_ledgers(entries, child_row, parent_doc)
+		doc = create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse)
 
 	return doc
 
 
-def create_serial_batch_no_ledgers(entries, child_row, parent_doc) -> object:
+def create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object:
 
-	warehouse = child_row.rejected_warhouse if child_row.is_rejected else child_row.warehouse
+	warehouse = warehouse or (
+		child_row.rejected_warehouse if child_row.is_rejected else child_row.warehouse
+	)
 
 	type_of_transaction = child_row.type_of_transaction
 	if parent_doc.get("doctype") == "Stock Entry":
 		type_of_transaction = "Outward" if child_row.s_warehouse else "Inward"
-		warehouse = child_row.s_warehouse or child_row.t_warehouse
+		warehouse = warehouse or child_row.s_warehouse or child_row.t_warehouse
 
 	doc = frappe.get_doc(
 		{
@@ -977,11 +979,12 @@
 	return doc
 
 
-def update_serial_batch_no_ledgers(entries, child_row, parent_doc) -> object:
+def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object:
 	doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
 	doc.voucher_detail_no = child_row.name
 	doc.posting_date = parent_doc.posting_date
 	doc.posting_time = parent_doc.posting_time
+	doc.warehouse = warehouse or doc.warehouse
 	doc.set("entries", [])
 
 	for d in entries:
@@ -989,7 +992,7 @@
 			"entries",
 			{
 				"qty": d.get("qty") * (1 if doc.type_of_transaction == "Inward" else -1),
-				"warehouse": d.get("warehouse"),
+				"warehouse": warehouse or d.get("warehouse"),
 				"batch_no": d.get("batch_no"),
 				"serial_no": d.get("serial_no"),
 			},
@@ -1223,13 +1226,14 @@
 
 def get_auto_batch_nos(kwargs):
 	available_batches = get_available_batches(kwargs)
-
 	qty = flt(kwargs.qty)
 
 	stock_ledgers_batches = get_stock_ledgers_batches(kwargs)
 	if stock_ledgers_batches:
 		update_available_batches(available_batches, stock_ledgers_batches)
 
+	available_batches = list(filter(lambda x: x.qty > 0, available_batches))
+
 	if not qty:
 		return available_batches
 
@@ -1264,9 +1268,15 @@
 
 
 def update_available_batches(available_batches, reserved_batches):
-	for batch in available_batches:
-		if batch.batch_no and batch.batch_no in reserved_batches:
-			batch.qty -= reserved_batches[batch.batch_no]
+	for batch_no, data in reserved_batches.items():
+		batch_not_exists = True
+		for batch in available_batches:
+			if batch.batch_no == batch_no:
+				batch.qty += data.qty
+				batch_not_exists = False
+
+		if batch_not_exists:
+			available_batches.append(data)
 
 
 def get_available_batches(kwargs):
@@ -1287,7 +1297,7 @@
 		)
 		.where(((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull())))
 		.where(stock_ledger_entry.is_cancelled == 0)
-		.groupby(batch_ledger.batch_no)
+		.groupby(batch_ledger.batch_no, batch_ledger.warehouse)
 	)
 
 	if kwargs.get("posting_date"):
@@ -1326,7 +1336,6 @@
 		query = query.where(stock_ledger_entry.voucher_no.notin(kwargs.get("ignore_voucher_nos")))
 
 	data = query.run(as_dict=True)
-	data = list(filter(lambda x: x.qty > 0, data))
 
 	return data
 
@@ -1452,9 +1461,12 @@
 
 def get_stock_ledgers_batches(kwargs):
 	stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
+	batch_table = frappe.qb.DocType("Batch")
 
 	query = (
 		frappe.qb.from_(stock_ledger_entry)
+		.inner_join(batch_table)
+		.on(stock_ledger_entry.batch_no == batch_table.name)
 		.select(
 			stock_ledger_entry.warehouse,
 			stock_ledger_entry.item_code,
@@ -1474,10 +1486,16 @@
 		else:
 			query = query.where(stock_ledger_entry[field] == kwargs.get(field))
 
-	data = query.run(as_dict=True)
+	if kwargs.based_on == "LIFO":
+		query = query.orderby(batch_table.creation, order=frappe.qb.desc)
+	elif kwargs.based_on == "Expiry":
+		query = query.orderby(batch_table.expiry_date)
+	else:
+		query = query.orderby(batch_table.creation)
 
-	batches = defaultdict(float)
+	data = query.run(as_dict=True)
+	batches = {}
 	for d in data:
-		batches[d.batch_no] += d.qty
+		batches[d.batch_no] = d
 
 	return batches
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 2f49822..f19df83 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -2351,7 +2351,11 @@
 			return
 
 		for d in self.items:
-			if d.is_finished_item and d.item_code == self.pro_doc.production_item:
+			if (
+				d.is_finished_item
+				and d.item_code == self.pro_doc.production_item
+				and not d.serial_and_batch_bundle
+			):
 				serial_nos = self.get_available_serial_nos()
 				if serial_nos:
 					row = frappe._dict({"serial_nos": serial_nos[0 : cint(d.qty)]})
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index e25c843..3b6db64 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -94,6 +94,7 @@
 			frappe.enqueue(
 				"erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions",
 				now=frappe.flags.in_test,
+				enqueue_after_commit=True,
 			)
 
 	def validate_pending_reposts(self):
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 9c55358..a75c3b0 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -738,6 +738,7 @@
 			return frappe._dict({})
 
 		doc.save()
+		self.validate_qty(doc)
 
 		if not hasattr(self, "do_not_submit") or not self.do_not_submit:
 			doc.flags.ignore_voucher_validation = True
@@ -767,6 +768,17 @@
 		doc.save()
 		return doc
 
+	def validate_qty(self, doc):
+		if doc.type_of_transaction == "Outward":
+			precision = doc.precision("total_qty")
+
+			total_qty = abs(flt(doc.total_qty, precision))
+			required_qty = abs(flt(self.actual_qty, precision))
+
+			if required_qty - total_qty > 0:
+				msg = f"For the item {bold(doc.item_code)}, the Avaliable qty {bold(total_qty)} is less than the Required Qty {bold(required_qty)} in the warehouse {bold(doc.warehouse)}. Please add sufficient qty in the warehouse."
+				frappe.throw(msg, title=_("Insufficient Stock"))
+
 	def set_auto_serial_batch_entries_for_outward(self):
 		from erpnext.stock.doctype.batch.batch import get_available_batches
 		from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward