fix: POS return for Serialized Items (#24292)

Co-authored-by: Nabin Hait <nabinhait@gmail.com>
Co-authored-by: Saqib <nextchamp.saqib@gmail.com>
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 94573f9..76e0092 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -179,10 +179,18 @@
 			if d.get("serial_no"):
 				serial_nos = get_serial_nos(d.serial_no)
 				for sr in serial_nos:
-					serial_no_exists = frappe.db.exists("POS Invoice Item", {
-						"parent": self.return_against, 
-						"serial_no": ["like", d.get("serial_no")]
-					})
+					serial_no_exists = frappe.db.sql("""
+						SELECT name
+						FROM `tabPOS Invoice Item`
+						WHERE
+							parent = %s
+							and (serial_no = %s
+								or serial_no like %s
+								or serial_no like %s
+								or serial_no like %s
+							)
+					""", (self.return_against, sr, sr+'\n%', '%\n'+sr, '%\n'+sr+'\n%'))
+
 					if not serial_no_exists:
 						bold_return_against = frappe.bold(self.return_against)
 						bold_serial_no = frappe.bold(sr)
@@ -190,7 +198,7 @@
 							_("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}")
 							.format(d.idx, bold_serial_no, bold_return_against)
 						)
-	
+
 	def validate_non_stock_items(self):
 		for d in self.get("items"):
 			is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
@@ -292,7 +300,7 @@
 
 		if not self.get('payments') and not for_validate:
 			update_multi_mode_option(self, profile)
-		
+
 		if self.is_return and not for_validate:
 			add_return_modes(self, profile)
 
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 57a23af..15875af 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -198,6 +198,65 @@
 		self.assertEqual(pos_return.get('payments')[0].amount, -500)
 		self.assertEqual(pos_return.get('payments')[1].amount, -500)
 
+	def test_pos_return_for_serialized_item(self):
+		from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+		from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+		se = make_serialized_item(company='_Test Company',
+			target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
+
+		serial_nos = get_serial_nos(se.get("items")[0].serial_no)
+
+		pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
+			account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
+			expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
+			item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
+
+		pos.get("items")[0].serial_no = serial_nos[0]
+		pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1})
+
+		pos.insert()
+		pos.submit()
+
+		pos_return = make_sales_return(pos.name)
+
+		pos_return.insert()
+		pos_return.submit()
+		self.assertEqual(pos_return.get('items')[0].serial_no, serial_nos[0])
+
+	def test_partial_pos_returns(self):
+		from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+		from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+		se = make_serialized_item(company='_Test Company',
+			target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
+
+		serial_nos = get_serial_nos(se.get("items")[0].serial_no)
+
+		pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
+			account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
+			expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
+			item=se.get("items")[0].item_code, qty=2, rate=1000, do_not_save=1)
+
+		pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[1]
+		pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1})
+
+		pos.insert()
+		pos.submit()
+
+		pos_return1 = make_sales_return(pos.name)
+
+		# partial return 1
+		pos_return1.get('items')[0].qty = -1
+		pos_return1.get('items')[0].serial_no = serial_nos[0]
+		pos_return1.insert()
+		pos_return1.submit()
+
+		# partial return 2
+		pos_return2 = make_sales_return(pos.name)
+		self.assertEqual(pos_return2.get('items')[0].qty, -1)
+		self.assertEqual(pos_return2.get('items')[0].serial_no, serial_nos[1])
+
 	def test_pos_change_amount(self):
 		pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC",
 			income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
index 2b6e7de..8b71eb0 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
@@ -87,6 +87,7 @@
   "edit_references",
   "sales_order",
   "so_detail",
+  "pos_invoice_item",
   "column_break_74",
   "delivery_note",
   "dn_detail",
@@ -790,11 +791,20 @@
    "fieldtype": "Link",
    "label": "Project",
    "options": "Project"
+  },
+  {
+   "fieldname": "pos_invoice_item",
+   "fieldtype": "Data",
+   "ignore_user_permissions": 1,
+   "label": "POS Invoice Item",
+   "no_copy": 1,
+   "print_hide": 1,
+   "read_only": 1
   }
  ],
  "istable": 1,
  "links": [],
- "modified": "2020-07-22 13:40:34.418346",
+ "modified": "2021-01-04 17:34:49.924531",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "POS Invoice Item",
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 c88d679..40f77b4 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
@@ -29,7 +29,7 @@
 		for d in self.pos_invoices:
 			status, docstatus, is_return, return_against = frappe.db.get_value(
 				'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against'])
-			
+
 			bold_pos_invoice = frappe.bold(d.pos_invoice)
 			bold_status = frappe.bold(status)
 			if docstatus != 1:
@@ -58,7 +58,7 @@
 		sales_invoice, credit_note = "", ""
 		if sales:
 			sales_invoice = self.process_merging_into_sales_invoice(sales)
-		
+
 		if returns:
 			credit_note = self.process_merging_into_credit_note(returns)
 
@@ -74,7 +74,7 @@
 
 	def process_merging_into_sales_invoice(self, data):
 		sales_invoice = self.get_new_sales_invoice()
-		
+
 		sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
 
 		sales_invoice.is_consolidated = 1
@@ -98,19 +98,19 @@
 		self.consolidated_credit_note = credit_note.name
 
 		return credit_note.name
-	
+
 	def merge_pos_invoice_into(self, invoice, data):
 		items, payments, taxes = [], [], []
 		loyalty_amount_sum, loyalty_points_sum = 0, 0
 		for doc in data:
 			map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
-			
+
 			if doc.redeem_loyalty_points:
 				invoice.loyalty_redemption_account = doc.loyalty_redemption_account
 				invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center
 				loyalty_points_sum += doc.loyalty_points
 				loyalty_amount_sum += doc.loyalty_amount
-			
+
 			for item in doc.get('items'):
 				found = False
 				for i in items:
@@ -118,12 +118,13 @@
 						i.uom == item.uom and i.net_rate == item.net_rate):
 						found = True
 						i.qty = i.qty + item.qty
+
 				if not found:
 					item.rate = item.net_rate
 					item.price_list_rate = 0
 					si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
 					items.append(si_item)
-			
+
 			for tax in doc.get('taxes'):
 				found = False
 				for t in taxes:
@@ -162,7 +163,7 @@
 		invoice.ignore_pricing_rule = 1
 
 		return invoice
-	
+
 	def get_new_sales_invoice(self):
 		sales_invoice = frappe.new_doc('Sales Invoice')
 		sales_invoice.customer = self.customer
@@ -194,7 +195,7 @@
 	}
 	pos_invoices = frappe.db.get_all('POS Invoice', filters=filters,
 		fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer'])
-	
+
 	return pos_invoices
 
 def get_invoice_customer_map(pos_invoices):
@@ -204,7 +205,7 @@
 		customer = invoice.get('customer')
 		pos_invoice_customer_map.setdefault(customer, [])
 		pos_invoice_customer_map[customer].append(invoice)
-	
+
 	return pos_invoice_customer_map
 
 def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 0e1829a..de61b35 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -204,8 +204,6 @@
 	return items
 
 def get_returned_qty_map_for_row(row_name, doctype):
-	if doctype == "POS Invoice": return {}
-
 	child_doctype = doctype + " Item"
 	reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
 
@@ -354,7 +352,12 @@
 			target_doc.so_detail = source_doc.so_detail
 			target_doc.dn_detail = source_doc.dn_detail
 			target_doc.expense_account = source_doc.expense_account
-			target_doc.sales_invoice_item = source_doc.name
+
+			if doctype == "Sales Invoice":
+				target_doc.sales_invoice_item = source_doc.name
+			else:
+				target_doc.pos_invoice_item = source_doc.name
+
 			target_doc.price_list_rate = 0
 			if default_warehouse_for_sales_return:
 				target_doc.warehouse = default_warehouse_for_sales_return