Merge pull request #35977 from rohitwaghchaure/reserve-pos-invoice-batches

fix: reserve the pos invoice batches
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
index cced375..32e267f 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -20,7 +20,7 @@
 
 	onload(doc) {
 		super.onload();
-		this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log', 'POS Closing Entry'];
+		this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log', 'POS Closing Entry', 'Serial and Batch Bundle'];
 
 		if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
 			this.frm.script_manager.trigger("is_pos");
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index bf393c0..4b2fcec 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -93,7 +93,7 @@
 			)
 
 	def on_cancel(self):
-		self.ignore_linked_doctypes = "Payment Ledger Entry"
+		self.ignore_linked_doctypes = ["Payment Ledger Entry", "Serial and Batch Bundle"]
 		# run on cancel method of selling controller
 		super(SalesInvoice, self).on_cancel()
 		if not self.is_return and self.loyalty_program:
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index f842a16..0fce61f 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -767,6 +767,39 @@
 		)
 		self.assertEqual(rounded_total, 400)
 
+	def test_pos_batch_reservation(self):
+		from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+			get_auto_batch_nos,
+		)
+		from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+			create_batch_item_with_batch,
+		)
+
+		create_batch_item_with_batch("_BATCH ITEM Test For Reserve", "TestBatch-RS 02")
+		make_stock_entry(
+			target="_Test Warehouse - _TC",
+			item_code="_BATCH ITEM Test For Reserve",
+			qty=20,
+			basic_rate=100,
+			batch_no="TestBatch-RS 02",
+		)
+
+		pos_inv1 = create_pos_invoice(
+			item="_BATCH ITEM Test For Reserve", rate=300, qty=15, batch_no="TestBatch-RS 02"
+		)
+		pos_inv1.save()
+		pos_inv1.submit()
+
+		batches = get_auto_batch_nos(
+			frappe._dict(
+				{"item_code": "_BATCH ITEM Test For Reserve", "warehouse": "_Test Warehouse - _TC"}
+			)
+		)
+
+		for batch in batches:
+			if batch.batch_no == "TestBatch-RS 02" and batch.warehouse == "_Test Warehouse - _TC":
+				self.assertEqual(batch.qty, 5)
+
 	def test_pos_batch_item_qty_validation(self):
 		from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
 			BatchNegativeStockError,
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 2776a74..75b6ec7 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
@@ -1241,59 +1241,125 @@
 	return list(set(ignore_serial_nos) - set(returned_serial_nos))
 
 
+def get_reserved_batches_for_pos(kwargs):
+	pos_batches = frappe._dict()
+	pos_invoices = frappe.get_all(
+		"POS Invoice",
+		fields=[
+			"`tabPOS Invoice Item`.batch_no",
+			"`tabPOS Invoice`.is_return",
+			"`tabPOS Invoice Item`.warehouse",
+			"`tabPOS Invoice Item`.name as child_docname",
+			"`tabPOS Invoice`.name as parent_docname",
+			"`tabPOS Invoice Item`.serial_and_batch_bundle",
+		],
+		filters=[
+			["POS Invoice", "consolidated_invoice", "is", "not set"],
+			["POS Invoice", "docstatus", "=", 1],
+			["POS Invoice Item", "item_code", "=", kwargs.item_code],
+			["POS Invoice", "name", "!=", kwargs.ignore_voucher_no],
+		],
+	)
+
+	ids = [
+		pos_invoice.serial_and_batch_bundle
+		for pos_invoice in pos_invoices
+		if pos_invoice.serial_and_batch_bundle
+	]
+
+	if not ids:
+		return []
+
+	if ids:
+		for d in get_serial_batch_ledgers(kwargs.item_code, docstatus=1, name=ids):
+			if d.batch_no not in pos_batches:
+				pos_batches[d.batch_no] = frappe._dict(
+					{
+						"qty": d.qty,
+						"warehouse": d.warehouse,
+					}
+				)
+			else:
+				pos_batches[d.batch_no].qty += d.qty
+
+	for row in pos_invoices:
+		if not row.batch_no:
+			continue
+
+		if row.batch_no in pos_batches:
+			pos_batches[row.batch_no] -= row.qty * -1 if row.is_return else row.qty
+		else:
+			pos_batches[row.batch_no] = frappe._dict(
+				{
+					"qty": (row.qty * -1 if row.is_return else row.qty),
+					"warehouse": row.warehouse,
+				}
+			)
+
+	return pos_batches
+
+
 def get_auto_batch_nos(kwargs):
 	available_batches = get_available_batches(kwargs)
 	qty = flt(kwargs.qty)
 
+	pos_invoice_batches = get_reserved_batches_for_pos(kwargs)
 	stock_ledgers_batches = get_stock_ledgers_batches(kwargs)
-	if stock_ledgers_batches:
-		update_available_batches(available_batches, stock_ledgers_batches)
+	if stock_ledgers_batches or pos_invoice_batches:
+		update_available_batches(available_batches, stock_ledgers_batches, pos_invoice_batches)
 
 	available_batches = list(filter(lambda x: x.qty > 0, available_batches))
-
 	if not qty:
 		return available_batches
 
+	return get_qty_based_available_batches(available_batches, qty)
+
+
+def get_qty_based_available_batches(available_batches, qty):
 	batches = []
 	for batch in available_batches:
-		if qty > 0:
-			batch_qty = flt(batch.qty)
-			if qty > batch_qty:
-				batches.append(
-					frappe._dict(
-						{
-							"batch_no": batch.batch_no,
-							"qty": batch_qty,
-							"warehouse": batch.warehouse,
-						}
-					)
+		if qty <= 0:
+			break
+
+		batch_qty = flt(batch.qty)
+		if qty > batch_qty:
+			batches.append(
+				frappe._dict(
+					{
+						"batch_no": batch.batch_no,
+						"qty": batch_qty,
+						"warehouse": batch.warehouse,
+					}
 				)
-				qty -= batch_qty
-			else:
-				batches.append(
-					frappe._dict(
-						{
-							"batch_no": batch.batch_no,
-							"qty": qty,
-							"warehouse": batch.warehouse,
-						}
-					)
+			)
+			qty -= batch_qty
+		else:
+			batches.append(
+				frappe._dict(
+					{
+						"batch_no": batch.batch_no,
+						"qty": qty,
+						"warehouse": batch.warehouse,
+					}
 				)
-				qty = 0
+			)
+			qty = 0
 
 	return batches
 
 
-def update_available_batches(available_batches, reserved_batches):
-	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
+def update_available_batches(available_batches, reserved_batches=None, pos_invoice_batches=None):
+	for batches in [reserved_batches, pos_invoice_batches]:
+		if batches:
+			for batch_no, data in batches.items():
+				batch_not_exists = True
+				for batch in available_batches:
+					if batch.batch_no == batch_no and batch.warehouse == data.warehouse:
+						batch.qty += data.qty
+						batch_not_exists = False
 
-		if batch_not_exists:
-			available_batches.append(data)
+				if batch_not_exists:
+					available_batches.append(data)
 
 
 def get_available_batches(kwargs):