fix: Handle rows with same item code from Purchase Receipt to Invoice. (#20724)

* fix: Handle rows with same item code from Purchase Receipt to Invoice.

* fix: Added patch, fixed tests, fixed delivery note behaviour

* chore: Added comments amd fixed typo

* fix: Added patch to patches.txt

* fix: Patch fix and simplification, json timestamp updation.

Co-authored-by: Nabin Hait <nabinhait@gmail.com>
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 81fdbbe..90c67f1 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -74,7 +74,7 @@
 	for d in doc.get("items"):
 		if d.item_code and (flt(d.qty) < 0 or flt(d.get('received_qty')) < 0):
 			if d.item_code not in valid_items:
-				frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}")
+				frappe.throw(_("Row # {0}: Returned Item {1} does not exist in {2} {3}")
 					.format(d.idx, d.item_code, doc.doctype, doc.return_against))
 			else:
 				ref = valid_items.get(d.item_code, frappe._dict())
@@ -266,6 +266,8 @@
 			target_doc.purchase_order = source_doc.purchase_order
 			target_doc.purchase_order_item = source_doc.purchase_order_item
 			target_doc.rejected_warehouse = source_doc.rejected_warehouse
+			target_doc.purchase_receipt_item = source_doc.name
+
 		elif doctype == "Purchase Invoice":
 			target_doc.received_qty = -1* source_doc.received_qty
 			target_doc.rejected_qty = -1* source_doc.rejected_qty
@@ -282,6 +284,7 @@
 			target_doc.so_detail = source_doc.so_detail
 			target_doc.si_detail = source_doc.si_detail
 			target_doc.expense_account = source_doc.expense_account
+			target_doc.dn_detail = source_doc.name
 			if default_warehouse_for_sales_return:
 				target_doc.warehouse = default_warehouse_for_sales_return
 		elif doctype == "Sales Invoice":
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 5295399..8a31cf3 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -662,6 +662,7 @@
 erpnext.patches.v12_0.move_bank_account_swift_number_to_bank
 erpnext.patches.v12_0.rename_bank_reconciliation
 erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22
+erpnext.patches.v12_0.set_purchase_receipt_delivery_note_detail
 erpnext.patches.v12_0.add_permission_in_lower_deduction
 erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom
 erpnext.patches.v12_0.rename_account_type_doctype
diff --git a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py
new file mode 100644
index 0000000..f5bd8c3
--- /dev/null
+++ b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py
@@ -0,0 +1,84 @@
+from __future__ import unicode_literals
+import frappe
+from collections import defaultdict
+
+def execute():
+	def map_rows(doc_row, return_doc_row, detail_field, doctype):
+		"""Map rows after identifying similar ones."""
+
+		frappe.db.sql(""" UPDATE `tab{doctype} Item` set {detail_field} = '{doc_row_name}'
+				where name = '{return_doc_row_name}'""" \
+			.format(doctype=doctype,
+					detail_field=detail_field,
+					doc_row_name=doc_row.get('name'),
+					return_doc_row_name=return_doc_row.get('name'))) #nosec
+
+	def row_is_mappable(doc_row, return_doc_row, detail_field):
+		"""Checks if two rows are similar enough to be mapped."""
+
+		if doc_row.item_code == return_doc_row.item_code and not return_doc_row.get(detail_field):
+			if doc_row.get('batch_no') and return_doc_row.get('batch_no') and doc_row.batch_no == return_doc_row.batch_no:
+				return True
+
+			elif doc_row.get('serial_no') and return_doc_row.get('serial_no'):
+				doc_sn = doc_row.serial_no.split('\n')
+				return_doc_sn = return_doc_row.serial_no.split('\n')
+
+				if set(doc_sn) & set(return_doc_sn):
+					# if two rows have serial nos in common, map them
+					return True
+
+			elif doc_row.rate == return_doc_row.rate:
+				return True
+		else:
+			return False
+
+	def make_return_document_map(doctype, return_document_map):
+		"""Returns a map of documents and it's return documents.
+		Format => { 'document' : ['return_document_1','return_document_2'] }"""
+
+		return_against_documents = frappe.db.sql("""
+			SELECT
+				return_against as document, name as return_document
+			FROM `tab{doctype}`
+			WHERE
+				is_return = 1 and docstatus = 1""".format(doctype=doctype),as_dict=1) #nosec
+
+		for entry in return_against_documents:
+			return_document_map[entry.document].append(entry.return_document)
+
+		return return_document_map
+
+	def set_document_detail_in_return_document(doctype):
+		"""Map each row of the original document in the return document."""
+		mapped = []
+		return_document_map = defaultdict(list)
+		detail_field = "purchase_receipt_item" if doctype=="Purchase Receipt" else "dn_detail"
+
+		child_doc = frappe.scrub("{0} Item".format(doctype))
+		frappe.reload_doc("stock", "doctype", child_doc)
+
+		return_document_map = make_return_document_map(doctype, return_document_map)
+
+		#iterate through original documents and its return documents
+		for docname in return_document_map:
+			doc_items = frappe.get_doc(doctype, docname).get("items")
+			for return_doc in return_document_map[docname]:
+				return_doc_items = frappe.get_doc(doctype, return_doc).get("items")
+
+				#iterate through return document items and original document items for mapping
+				for return_item in return_doc_items:
+					for doc_item in doc_items:
+						if row_is_mappable(doc_item, return_item, detail_field) and doc_item.get('name') not in mapped:
+							map_rows(doc_item, return_item, detail_field, doctype)
+							mapped.append(doc_item.get('name'))
+							break
+						else:
+							continue
+
+	set_document_detail_in_return_document("Purchase Receipt")
+	set_document_detail_in_return_document("Delivery Note")
+	frappe.db.commit()
+
+
+
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 37f9097..d04cf78 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -388,13 +388,12 @@
 
 def get_returned_qty_map(delivery_note):
 	"""returns a map: {so_detail: returned_qty}"""
-	returned_qty_map = frappe._dict(frappe.db.sql("""select dn_item.item_code, sum(abs(dn_item.qty)) as qty
+	returned_qty_map = frappe._dict(frappe.db.sql("""select dn_item.dn_detail, abs(dn_item.qty) as qty
 		from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn
 		where dn.name = dn_item.parent
 			and dn.docstatus = 1
 			and dn.is_return = 1
 			and dn.return_against = %s
-		group by dn_item.item_code
 	""", delivery_note))
 
 	return returned_qty_map
@@ -413,7 +412,7 @@
 		target.run_method("set_po_nos")
 
 		if len(target.get("items")) == 0:
-			frappe.throw(_("All these items have already been invoiced"))
+			frappe.throw(_("All these items have already been Invoiced/Returned"))
 
 		target.run_method("calculate_taxes_and_totals")
 
@@ -438,9 +437,9 @@
 		pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0)
 
 		returned_qty = 0
-		if returned_qty_map.get(item_row.item_code, 0) > 0:
-			returned_qty = flt(returned_qty_map.get(item_row.item_code, 0))
-			returned_qty_map[item_row.item_code] -= pending_qty
+		if returned_qty_map.get(item_row.name, 0) > 0:
+			returned_qty = flt(returned_qty_map.get(item_row.name, 0))
+			returned_qty_map[item_row.name] -= pending_qty
 
 		if returned_qty:
 			if returned_qty >= pending_qty:
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index bf7007a..a921a56 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -612,6 +612,7 @@
 		dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-1, do_not_submit=True)
 		dn1.items[0].against_sales_order = so.name
 		dn1.items[0].so_detail = so.items[0].name
+		dn1.items[0].dn_detail = dn.items[0].name
 		dn1.submit()
 
 		si = make_sales_invoice(dn.name)
@@ -638,7 +639,9 @@
 		si1.save()
 		si1.submit()
 
-		create_delivery_note(is_return=1, return_against=dn.name, qty=-2)
+		dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, do_not_submit=True)
+		dn1.items[0].dn_detail = dn.items[0].name
+		dn1.submit()
 
 		si2 = make_sales_invoice(dn.name)
 		self.assertEquals(si2.items[0].qty, 2)
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index 782ac84..7ea2de2 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -67,6 +67,7 @@
   "so_detail",
   "against_sales_invoice",
   "si_detail",
+  "dn_detail",
   "section_break_40",
   "batch_no",
   "serial_no",
@@ -699,6 +700,15 @@
   {
    "fieldname": "dimension_col_break",
    "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "dn_detail",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Against Delivery Note Item",
+   "no_copy": 1,
+   "print_hide": 1,
+   "read_only": 1
   }
  ],
  "idx": 1,
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 8dfe1d1..e6ab8d6 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -504,7 +504,7 @@
 
 	def set_missing_values(source, target):
 		if len(target.get("items")) == 0:
-			frappe.throw(_("All items have already been invoiced"))
+			frappe.throw(_("All items have already been Invoiced/Returned"))
 
 		doc = frappe.get_doc(target)
 		doc.ignore_pricing_rule = 1
@@ -514,11 +514,11 @@
 
 	def update_item(source_doc, target_doc, source_parent):
 		target_doc.qty, returned_qty = get_pending_qty(source_doc)
-		returned_qty_map[source_doc.item_code] = returned_qty
+		returned_qty_map[source_doc.name] = returned_qty
 
 	def get_pending_qty(item_row):
 		pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0)
-		returned_qty = flt(returned_qty_map.get(item_row.item_code, 0))
+		returned_qty = flt(returned_qty_map.get(item_row.name, 0))
 		if returned_qty:
 			if returned_qty >= pending_qty:
 				pending_qty = 0
@@ -576,13 +576,12 @@
 
 def get_returned_qty_map(purchase_receipt):
 	"""returns a map: {so_detail: returned_qty}"""
-	returned_qty_map = frappe._dict(frappe.db.sql("""select pr_item.item_code, sum(abs(pr_item.qty)) as qty
+	returned_qty_map = frappe._dict(frappe.db.sql("""select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty
 		from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
 		where pr.name = pr_item.parent
 			and pr.docstatus = 1
 			and pr.is_return = 1
 			and pr.return_against = %s
-		group by pr_item.item_code
 	""", purchase_receipt))
 
 	return returned_qty_map
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 3d42590..649cfdc 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -475,6 +475,7 @@
 		pr1 = make_purchase_receipt(is_return=1, return_against=pr.name, qty=-1, do_not_submit=True)
 		pr1.items[0].purchase_order = po.name
 		pr1.items[0].purchase_order_item = po.items[0].name
+		pr1.items[0].purchase_receipt_item = pr.items[0].name
 		pr1.submit()
 
 		pi = make_purchase_invoice(pr.name)
@@ -498,7 +499,9 @@
 		pi1.save()
 		pi1.submit()
 
-		make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2)
+		pr2 = make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2, do_not_submit=True)
+		pr2.items[0].purchase_receipt_item = pr1.items[0].name
+		pr2.submit()
 
 		pi2 = make_purchase_invoice(pr1.name)
 		self.assertEquals(pi2.items[0].qty, 2)
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index bc6bce9..c1e1f90 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -71,6 +71,7 @@
   "quality_inspection",
   "purchase_order_item",
   "material_request_item",
+  "purchase_receipt_item",
   "section_break_45",
   "allow_zero_valuation_rate",
   "bom",
@@ -821,6 +822,15 @@
    "options": "Warehouse"
   },
   {
+   "fieldname": "purchase_receipt_item",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Purchase Receipt Item",
+   "no_copy": 1,
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
    "collapsible": 1,
    "fieldname": "image_column",
    "fieldtype": "Column Break"
@@ -829,7 +839,7 @@
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2020-04-10 19:01:21.154963",
+ "modified": "2020-04-28 19:01:21.154963",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Purchase Receipt Item",