Merge branch 'develop' into fix-pos-issues-again
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index 0c6e7ed..b850027 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -264,7 +264,6 @@
    "print_hide": 1
   },
   {
-   "allow_on_submit": 1,
    "default": "0",
    "fieldname": "is_return",
    "fieldtype": "Check",
@@ -1573,7 +1572,7 @@
  "icon": "fa fa-file-text",
  "is_submittable": 1,
  "links": [],
- "modified": "2021-10-05 12:11:53.871828",
+ "modified": "2022-03-22 13:00:24.166684",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "POS Invoice",
@@ -1623,6 +1622,7 @@
  "show_name_in_global_search": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "timeline_field": "customer",
  "title_field": "title",
  "track_changes": 1,
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index d782cc2..275eeb3 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -16,7 +16,11 @@
 )
 from erpnext.accounts.party import get_due_date, get_party_account
 from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
-from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
+from erpnext.stock.doctype.serial_no.serial_no import (
+	get_delivered_serial_nos,
+	get_pos_reserved_serial_nos,
+	get_serial_nos,
+)
 
 
 class POSInvoice(SalesInvoice):
@@ -145,12 +149,7 @@
 						.format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"))
 
 	def validate_delivered_serial_nos(self, item):
-		serial_nos = get_serial_nos(item.serial_no)
-		delivered_serial_nos = frappe.db.get_list('Serial No', {
-			'item_code': item.item_code,
-			'name': ['in', serial_nos],
-			'sales_invoice': ['is', 'set']
-		}, pluck='name')
+		delivered_serial_nos = get_delivered_serial_nos(item.serial_no)
 
 		if delivered_serial_nos:
 			bold_delivered_serial_nos = frappe.bold(', '.join(delivered_serial_nos))
@@ -172,10 +171,14 @@
 			frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
 
 	def validate_stock_availablility(self):
+		if self.is_return:
+			return
+
+		if self.docstatus.is_draft() and not frappe.db.get_value('POS Profile', self.pos_profile, 'validate_stock_on_save'):
+			return
+
 		from erpnext.stock.stock_ledger import is_negative_stock_allowed
 
-		if self.is_return or self.docstatus != 1:
-			return
 		for d in self.get('items'):
 			is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
 			if is_service_item:
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index cf8affd..e06f7aa 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -340,6 +340,7 @@
 			item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
 
 		si.get("items")[0].serial_no = serial_nos[0]
+		si.update_stock = 1
 		si.insert()
 		si.submit()
 
@@ -610,6 +611,78 @@
 			pos_inv.delete()
 			pr.delete()
 
+	def test_delivered_serial_no_case(self):
+		from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
+			init_user_and_profile,
+		)
+		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+		from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
+		from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+
+		frappe.db.savepoint('before_test_delivered_serial_no_case')
+		try:
+			se = make_serialized_item()
+			serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
+
+			dn = create_delivery_note(
+					item_code="_Test Serialized Item With Series", serial_no=serial_no
+			)
+
+			delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no")
+			self.assertEquals(delivery_document_no, dn.name)
+
+			init_user_and_profile()
+
+			pos_inv = create_pos_invoice(
+					item_code="_Test Serialized Item With Series",
+					serial_no=serial_no,
+					qty=1,
+					rate=100,
+					do_not_submit=True
+			)
+
+			self.assertRaises(frappe.ValidationError, pos_inv.submit)
+
+		finally:
+			frappe.db.rollback(save_point='before_test_delivered_serial_no_case')
+			frappe.set_user("Administrator")
+
+	def test_returned_serial_no_case(self):
+		from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
+			init_user_and_profile,
+		)
+		from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
+		from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
+		from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+
+		frappe.db.savepoint('before_test_returned_serial_no_case')
+		try:
+			se = make_serialized_item()
+			serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
+
+			init_user_and_profile()
+
+			pos_inv = create_pos_invoice(
+					item_code="_Test Serialized Item With Series",
+					serial_no=serial_no,
+					qty=1,
+					rate=100,
+			)
+
+			pos_return = make_sales_return(pos_inv.name)
+			pos_return.flags.ignore_validate = True
+			pos_return.insert()
+			pos_return.submit()
+
+			pos_reserved_serial_nos = get_pos_reserved_serial_nos({
+				'item_code': '_Test Serialized Item With Series',
+				'warehouse': '_Test Warehouse - _TC'
+			})
+			self.assertTrue(serial_no not in pos_reserved_serial_nos)
+
+		finally:
+			frappe.db.rollback(save_point='before_test_returned_serial_no_case')
+			frappe.set_user("Administrator")
 
 def create_pos_invoice(**args):
 	args = frappe._dict(args)
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index d4513c6..b3d9c15 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -53,7 +53,7 @@
 					frappe.throw(msg)
 
 	def on_submit(self):
-		pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
+		pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
 
 		returns = [d for d in pos_invoice_docs if d.get('is_return') == 1]
 		sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
@@ -70,7 +70,7 @@
 		self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
 
 	def on_cancel(self):
-		pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
+		pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
 
 		self.update_pos_invoices(pos_invoice_docs)
 		self.cancel_linked_invoices()
@@ -254,7 +254,7 @@
 		'docstatus': 1
 	}
 	pos_invoices = frappe.db.get_all('POS Invoice', filters=filters,
-		fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer'])
+		fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer', 'is_return', 'return_against'])
 
 	return pos_invoices
 
@@ -294,17 +294,62 @@
 	else:
 		cancel_merge_logs(merge_logs, closing_entry)
 
+def split_invoices(invoices):
+	'''
+	Splits invoices into multiple groups
+	Use-case:
+	If a serial no is sold and later it is returned
+	then split the invoices such that the selling entry is merged first and then the return entry
+	'''
+	# Input
+	# invoices = [
+	# 	{'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0},
+	# 	{'pos_invoice': 'Invoice with SR#1', 'is_return': 1},
+	# 	{'pos_invoice': 'Invoice with SR#2', 'is_return': 0}
+	# ]
+	# Output
+	# _invoices = [
+	# 	[{'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}],
+	# 	[{'pos_invoice': 'Invoice with SR#1', 'is_return': 1}, {'pos_invoice': 'Invoice with SR#2', 'is_return': 0}],
+	# ]
+
+	_invoices = []
+	special_invoices = []
+	pos_return_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in invoices if d.is_return and d.return_against]
+	for pos_invoice in pos_return_docs:
+		for item in pos_invoice.items:
+			if not item.serial_no:
+				continue
+
+			return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against)
+			if return_against_is_added:
+				break
+
+			return_against_is_consolidated = frappe.db.get_value('POS Invoice', pos_invoice.return_against, 'status', cache=True) == 'Consolidated'
+			if return_against_is_consolidated:
+				break
+
+			pos_invoice_row = [d for d in invoices if d.pos_invoice == pos_invoice.return_against]
+			_invoices.append(pos_invoice_row)
+			special_invoices.append(pos_invoice.return_against)
+			break
+
+	_invoices.append([d for d in invoices if d.pos_invoice not in special_invoices])
+
+	return _invoices
+
 def create_merge_logs(invoice_by_customer, closing_entry=None):
 	try:
 		for customer, invoices in invoice_by_customer.items():
-			merge_log = frappe.new_doc('POS Invoice Merge Log')
-			merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
-			merge_log.customer = customer
-			merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None
+			for _invoices in split_invoices(invoices):
+				merge_log = frappe.new_doc('POS Invoice Merge Log')
+				merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
+				merge_log.customer = customer
+				merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None
 
-			merge_log.set('pos_invoices', invoices)
-			merge_log.save(ignore_permissions=True)
-			merge_log.submit()
+				merge_log.set('pos_invoices', _invoices)
+				merge_log.save(ignore_permissions=True)
+				merge_log.submit()
 
 		if closing_entry:
 			closing_entry.set_status(update=True, status='Submitted')
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
index 8909da9..fe57ce2 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
@@ -385,4 +385,66 @@
 		finally:
 			frappe.set_user("Administrator")
 			frappe.db.sql("delete from `tabPOS Profile`")
+			frappe.db.sql("delete from `tabPOS Invoice`")
+
+	def test_serial_no_case_1(self):
+		'''
+		Create a POS Invoice with serial no
+		Create a Return Invoice with serial no
+		Create a POS Invoice with serial no again
+		Consolidate the invoices
+
+		The first POS Invoice should be consolidated with a separate single Merge Log
+		The second and third POS Invoice should be consolidated with a single Merge Log
+		'''
+
+		from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos
+		from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+
+		frappe.db.sql("delete from `tabPOS Invoice`")
+
+		try:
+			se = make_serialized_item()
+			serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
+
+			init_user_and_profile()
+
+			pos_inv = create_pos_invoice(
+					item_code="_Test Serialized Item With Series",
+					serial_no=serial_no,
+					qty=1,
+					rate=100,
+					do_not_submit=1
+			)
+			pos_inv.append('payments', {
+				'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100
+			})
+			pos_inv.submit()
+
+			pos_inv_cn = make_sales_return(pos_inv.name)
+			pos_inv_cn.paid_amount = -100
+			pos_inv_cn.submit()
+
+			pos_inv2 = create_pos_invoice(
+					item_code="_Test Serialized Item With Series",
+					serial_no=serial_no,
+					qty=1,
+					rate=100,
+					do_not_submit=1
+			)
+			pos_inv2.append('payments', {
+				'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100
+			})
+			pos_inv2.submit()
+
+			consolidate_pos_invoices()
+
+			pos_inv.load_from_db()
+			pos_inv2.load_from_db()
+
+			self.assertNotEqual(pos_inv.consolidated_invoice, pos_inv2.consolidated_invoice)
+
+		finally:
+			frappe.set_user("Administrator")
+			frappe.db.sql("delete from `tabPOS Profile`")
 			frappe.db.sql("delete from `tabPOS Invoice`")
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json
index 205c4ed..387c4b0 100644
--- a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json
+++ b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json
@@ -9,7 +9,9 @@
   "posting_date",
   "column_break_3",
   "customer",
-  "grand_total"
+  "grand_total",
+  "is_return",
+  "return_against"
  ],
  "fields": [
   {
@@ -48,11 +50,27 @@
    "in_list_view": 1,
    "label": "Amount",
    "reqd": 1
+  },
+  {
+   "default": "0",
+   "fetch_from": "pos_invoice.is_return",
+   "fieldname": "is_return",
+   "fieldtype": "Check",
+   "label": "Is Return",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "pos_invoice.return_against",
+   "fieldname": "return_against",
+   "fieldtype": "Link",
+   "label": "Return Against",
+   "options": "POS Invoice",
+   "read_only": 1
   }
  ],
  "istable": 1,
  "links": [],
- "modified": "2020-05-29 15:08:42.194979",
+ "modified": "2022-03-24 13:32:02.366257",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "POS Invoice Reference",
@@ -61,5 +79,6 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json
index 9c9f37b..11646a6 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.json
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json
@@ -22,6 +22,7 @@
   "hide_images",
   "hide_unavailable_items",
   "auto_add_item_to_cart",
+  "validate_stock_on_save",
   "column_break_16",
   "update_stock",
   "ignore_pricing_rule",
@@ -351,6 +352,12 @@
   {
    "fieldname": "column_break_25",
    "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "validate_stock_on_save",
+   "fieldtype": "Check",
+   "label": "Validate Stock on Save"
   }
  ],
  "icon": "icon-cog",
@@ -378,10 +385,11 @@
    "link_fieldname": "pos_profile"
   }
  ],
- "modified": "2021-10-14 14:17:00.469298",
+ "modified": "2022-03-21 13:29:28.480533",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "POS Profile",
+ "naming_rule": "Set by user",
  "owner": "Administrator",
  "permissions": [
   {
@@ -404,5 +412,6 @@
   }
  ],
  "sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 35aac54..ddb524c 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -360,4 +360,5 @@
 erpnext.patches.v14_0.delete_non_profit_doctypes
 erpnext.patches.v14_0.update_employee_advance_status
 erpnext.patches.v13_0.add_cost_center_in_loans
+erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
 erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
diff --git a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py
new file mode 100644
index 0000000..6c24f52
--- /dev/null
+++ b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py
@@ -0,0 +1,38 @@
+import frappe
+
+
+def execute():
+	'''
+	Fetch and Set is_return & return_against from POS Invoice in POS Invoice References table.
+	'''
+
+	POSClosingEntry = frappe.qb.DocType("POS Closing Entry")
+	open_pos_closing_entries = (
+		frappe.qb
+			.from_(POSClosingEntry)
+			.select(POSClosingEntry.name)
+			.where(POSClosingEntry.docstatus == 0)
+			.run(pluck=True)
+		)
+
+	if not open_pos_closing_entries:
+		return
+
+	POSInvoiceReference = frappe.qb.DocType("POS Invoice Reference")
+	POSInvoice = frappe.qb.DocType("POS Invoice")
+	pos_invoice_references = (
+		frappe.qb
+			.from_(POSInvoiceReference)
+			.join(POSInvoice)
+			.on(POSInvoiceReference.pos_invoice == POSInvoice.name)
+			.select(POSInvoiceReference.name, POSInvoice.is_return, POSInvoice.return_against)
+			.where(POSInvoiceReference.parent.isin(open_pos_closing_entries))
+			.run(as_dict=True)
+	)
+
+	for row in pos_invoice_references:
+		frappe.db.set_value("POS Invoice Reference", row.name, "is_return", row.is_return)
+		if row.is_return:
+			frappe.db.set_value("POS Invoice Reference", row.name, "return_against", row.return_against)
+		else:
+			frappe.db.set_value("POS Invoice Reference", row.name, "return_against", None)
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index 993c61d..67948d7 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -8,7 +8,7 @@
 from frappe.utils.nestedset import get_root_of
 
 from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
-from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups
+from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups
 
 
 def search_by_term(search_term, warehouse, price_list):
@@ -275,3 +275,16 @@
 		contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}])
 		frappe.db.set_value('Customer', customer, 'mobile_no', value)
 	contact_doc.save()
+
+@frappe.whitelist()
+def get_pos_profile_data(pos_profile):
+	pos_profile = frappe.get_doc('POS Profile', pos_profile)
+	pos_profile = pos_profile.as_dict()
+
+	_customer_groups_with_children = []
+	for row in pos_profile.customer_groups:
+		children = get_child_nodes('Customer Group', row.customer_group)
+		_customer_groups_with_children.extend(children)
+
+	pos_profile.customer_groups = _customer_groups_with_children
+	return pos_profile
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index ea8459f..6974bed 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -119,10 +119,15 @@
 			this.allow_negative_stock = flt(message.allow_negative_stock) || false;
 		});
 
-		frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => {
-			Object.assign(this.settings, profile);
-			this.settings.customer_groups = profile.customer_groups.map(group => group.customer_group);
-			this.make_app();
+		frappe.call({
+			method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data",
+			args: { "pos_profile": this.pos_profile },
+			callback: (res) => {
+				const profile = res.message;
+				Object.assign(this.settings, profile);
+				this.settings.customer_groups = profile.customer_groups.map(group => group.name);
+				this.make_app();
+			}
 		});
 	}
 
@@ -555,7 +560,7 @@
 				if (this.item_details.$component.is(':visible'))
 					this.edit_item_details_of(item_row);
 
-				if (this.check_serial_batch_selection_needed(item_row))
+				if (this.check_serial_batch_selection_needed(item_row) && !this.item_details.$component.is(':visible'))
 					this.edit_item_details_of(item_row);
 			}
 
@@ -704,7 +709,7 @@
 		frappe.dom.freeze();
 		const { doctype, name, current_item } = this.item_details;
 
-		frappe.model.set_value(doctype, name, 'qty', 0)
+		return frappe.model.set_value(doctype, name, 'qty', 0)
 			.then(() => {
 				frappe.model.clear_doc(doctype, name);
 				this.update_cart_html(current_item, true);
@@ -715,7 +720,14 @@
 	}
 
 	async save_and_checkout() {
-		this.frm.is_dirty() && await this.frm.save();
-		this.payment.checkout();
+		if (this.frm.is_dirty()) {
+			// only move to payment section if save is successful
+			frappe.route_hooks.after_save = () => this.payment.checkout();
+			return this.frm.save(
+				null, null, null, () => this.cart.toggle_checkout_btn(true) // show checkout button on error
+			);
+		} else {
+			this.payment.checkout();
+		}
 	}
 };
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js
index fb69b63..b75ffb2 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_details.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -60,12 +60,18 @@
 		return item && item.name == this.current_item.name;
 	}
 
-	toggle_item_details_section(item) {
+	async toggle_item_details_section(item) {
 		const current_item_changed = !this.compare_with_current_item(item);
 
 		// if item is null or highlighted cart item is clicked twice
 		const hide_item_details = !Boolean(item) || !current_item_changed;
 
+		if ((!hide_item_details && current_item_changed) || hide_item_details) {
+			// if item details is being closed OR if item details is opened but item is changed
+			// in both cases, if the current item is a serialized item, then validate and remove the item
+			await this.validate_serial_batch_item();
+		}
+
 		this.events.toggle_item_selector(!hide_item_details);
 		this.toggle_component(!hide_item_details);
 
@@ -83,7 +89,6 @@
 			this.render_form(item);
 			this.events.highlight_cart_item(item);
 		} else {
-			this.validate_serial_batch_item();
 			this.current_item = {};
 		}
 	}
@@ -103,11 +108,11 @@
 			(serialized && batched && (no_batch_selected || no_serial_selected))) {
 
 			frappe.show_alert({
-				message: __("Item will be removed since no serial / batch no selected."),
+				message: __("Item is removed since no serial / batch no selected."),
 				indicator: 'orange'
 			});
 			frappe.utils.play_sound("cancel");
-			this.events.remove_item_from_cart();
+			return this.events.remove_item_from_cart();
 		}
 	}
 
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index 1e9f6d7..b4ece46 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -170,20 +170,24 @@
     });
 
 		frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
-			if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
-				frappe.run_serially([
-					() => frm.doc.ignore_pricing_rule=1,
-					() => frm.trigger('ignore_pricing_rule'),
-					() => frm.doc.ignore_pricing_rule=0,
-					() => frm.trigger('apply_pricing_rule'),
-					() => frm.save(),
-					() => this.update_totals_section(frm.doc)
-				]);
-			} else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
-				frappe.show_alert({
-					message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
-					indicator: "orange"
-				});
+			if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) {
+				if (!frm.doc.ignore_pricing_rule) {
+					frm.applying_pos_coupon_code = true;
+					frappe.run_serially([
+						() => frm.doc.ignore_pricing_rule=1,
+						() => frm.trigger('ignore_pricing_rule'),
+						() => frm.doc.ignore_pricing_rule=0,
+						() => frm.trigger('apply_pricing_rule'),
+						() => frm.save(),
+						() => this.update_totals_section(frm.doc),
+						() => (frm.applying_pos_coupon_code = false)
+					]);
+				} else if (frm.doc.ignore_pricing_rule) {
+					frappe.show_alert({
+						message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
+						indicator: "orange"
+					});
+				}
 			}
 		});
 
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 4c06012..2808c21 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -611,25 +611,60 @@
 
 	return sorted([d.get('name') for d in serial_numbers])
 
+def get_delivered_serial_nos(serial_nos):
+	'''
+	Returns serial numbers that delivered from the list of serial numbers
+	'''
+	from frappe.query_builder.functions import Coalesce
+
+	SerialNo = frappe.qb.DocType("Serial No")
+	serial_nos = get_serial_nos(serial_nos)
+	query = frappe.qb.select(SerialNo.name).from_(SerialNo).where(
+		(SerialNo.name.isin(serial_nos))
+		& (Coalesce(SerialNo.delivery_document_type, "") != "")
+	)
+
+	result = query.run()
+	if result and len(result) > 0:
+		delivered_serial_nos = [row[0] for row in result]
+		return delivered_serial_nos
+
 @frappe.whitelist()
 def get_pos_reserved_serial_nos(filters):
 	if isinstance(filters, str):
 		filters = json.loads(filters)
 
-	pos_transacted_sr_nos = frappe.db.sql("""select item.serial_no as serial_no
-		from `tabPOS Invoice` p, `tabPOS Invoice Item` item
-		where p.name = item.parent
-		and p.consolidated_invoice is NULL
-		and p.docstatus = 1
-		and item.docstatus = 1
-		and item.item_code = %(item_code)s
-		and item.warehouse = %(warehouse)s
-		and item.serial_no is NOT NULL and item.serial_no != ''
-		""", filters, as_dict=1)
+	POSInvoice = frappe.qb.DocType("POS Invoice")
+	POSInvoiceItem = frappe.qb.DocType("POS Invoice Item")
+	query = frappe.qb.from_(
+		POSInvoice
+	).from_(
+		POSInvoiceItem
+	).select(
+		POSInvoice.is_return,
+		POSInvoiceItem.serial_no
+	).where(
+		(POSInvoice.name == POSInvoiceItem.parent)
+		& (POSInvoice.docstatus == 1)
+		& (POSInvoiceItem.docstatus == 1)
+		& (POSInvoiceItem.item_code == filters.get('item_code'))
+		& (POSInvoiceItem.warehouse == filters.get('warehouse'))
+		& (POSInvoiceItem.serial_no.isnotnull())
+		& (POSInvoiceItem.serial_no != '')
+	)
+
+	pos_transacted_sr_nos = query.run(as_dict=True)
 
 	reserved_sr_nos = []
+	returned_sr_nos = []
 	for d in pos_transacted_sr_nos:
-		reserved_sr_nos += get_serial_nos(d.serial_no)
+		if d.is_return == 0:
+			reserved_sr_nos += get_serial_nos(d.serial_no)
+		elif d.is_return == 1:
+			returned_sr_nos += get_serial_nos(d.serial_no)
+
+	for sr_no in returned_sr_nos:
+		reserved_sr_nos.remove(sr_no)
 
 	return reserved_sr_nos