Merge pull request #16745 from nabinhait/manage-returned-qty

fix: Consider returned qty while making invoice from DN / PR
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 8911ddf..b1a851a 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -201,7 +201,8 @@
 					get_query: function() {
 						var filters = {
 							docstatus: 1,
-							company: me.frm.doc.company
+							company: me.frm.doc.company,
+							is_return: 0
 						};
 						if(me.frm.doc.customer) filters["customer"] = me.frm.doc.customer;
 						return {
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 50996ed..1eb2b09 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -391,7 +391,7 @@
 
 	return invoiced_qty_map
 
-def get_returned_qty_map(sales_orders):
+def get_returned_qty_map_against_so(sales_orders):
 	"""returns a map: {so_detail: returned_qty}"""
 	returned_qty_map = {}
 
@@ -403,12 +403,25 @@
 
 	return returned_qty_map
 
+def get_returned_qty_map_against_dn(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
+		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
+
 @frappe.whitelist()
 def make_sales_invoice(source_name, target_doc=None):
 	doc = frappe.get_doc('Delivery Note', source_name)
 	sales_orders = [d.against_sales_order for d in doc.items]
-	returned_qty_map = get_returned_qty_map(sales_orders)
-
+	returned_qty_map_against_so = get_returned_qty_map_against_so(sales_orders)
+	returned_qty_map_against_dn = get_returned_qty_map_against_dn(source_name)
 	invoiced_qty_map = get_invoiced_qty_map(source_name)
 
 	def set_missing_values(source, target):
@@ -428,13 +441,26 @@
 			target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address))
 
 	def update_item(source_doc, target_doc, source_parent):
-		target_doc.qty = (source_doc.qty -
-			invoiced_qty_map.get(source_doc.name, 0) - returned_qty_map.get(source_doc.so_detail, 0))
+		target_doc.qty, returned_qty = get_pending_qty(source_doc)
+		if not source_doc.so_detail:
+			returned_qty_map_against_dn[source_doc.item_code] = returned_qty
 
 		if source_doc.serial_no and source_parent.per_billed > 0:
 			target_doc.serial_no = get_delivery_note_serial_no(source_doc.item_code,
 				target_doc.qty, source_parent.name)
 
+	def get_pending_qty(item_row):
+		pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) - returned_qty_map_against_so.get(item_row.so_detail, 0)
+		returned_qty = flt(returned_qty_map_against_dn.get(item_row.item_code, 0))
+		if not item_row.so_detail:
+			if returned_qty >= pending_qty:
+				pending_qty = 0
+				returned_qty -= pending_qty
+			else:
+				pending_qty -= returned_qty
+				returned_qty = 0
+		return pending_qty, returned_qty
+
 	doc = get_mapped_doc("Delivery Note", source_name, 	{
 		"Delivery Note": {
 			"doctype": "Sales Invoice",
@@ -453,7 +479,7 @@
 				"cost_center": "cost_center"
 			},
 			"postprocess": update_item,
-			"filter": lambda d: abs(d.qty) - abs(invoiced_qty_map.get(d.name, 0))<=0
+			"filter": lambda d: get_pending_qty(d)[0]<=0
 		},
 		"Sales Taxes and Charges": {
 			"doctype": "Sales Taxes and Charges",
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 0c5a71c..5c03121 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -636,7 +636,7 @@
 			self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
 
 		set_perpetual_inventory(0, company)
-	
+
 	def test_make_sales_invoice_from_dn_for_returned_qty(self):
 		from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
 		from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
@@ -655,6 +655,33 @@
 		si = make_sales_invoice(dn.name)
 		self.assertEquals(si.items[0].qty, 1)
 
+	def test_make_sales_invoice_from_dn_with_returned_qty_against_dn(self):
+		from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
+
+		dn = create_delivery_note(qty=8, do_not_submit=True)
+		dn.append("items", {
+			"item_code": "_Test Item",
+			"warehouse": "_Test Warehouse - _TC",
+			"qty": 1,
+			"rate": 100,
+			"conversion_factor": 1.0,
+			"expense_account": "Cost of Goods Sold - _TC",
+			"cost_center": "_Test Cost Center - _TC"
+		})
+		dn.submit()
+
+		si1 = make_sales_invoice(dn.name)
+		si1.items[0].qty = 4
+		si1.items.pop(1)
+		si1.save()
+		si1.submit()
+
+		create_delivery_note(is_return=1, return_against=dn.name, qty=-2)
+
+		si2 = make_sales_invoice(dn.name)
+		self.assertEquals(si2.items[0].qty, 2)
+		self.assertEquals(si2.items[1].qty, 1)
+
 def create_delivery_note(**args):
 	dn = frappe.new_doc("Delivery Note")
 	args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index f006d00..cb51e22 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -125,7 +125,7 @@
 		else:
 			self.status = "Completed"
 
-		
+
 		# Updating stock ledger should always be called after updating prevdoc status,
 		# because updating ordered qty, reserved_qty_for_subcontract in bin
 		# depends upon updated ordered qty in PO
@@ -311,7 +311,7 @@
 				"\n".join(warehouse_with_no_account))
 
 		return process_gl_map(gl_entries)
-		
+
 	def get_asset_gl_entry(self, gl_entries):
 		for d in self.get("items"):
 			if d.is_fixed_asset:
@@ -405,6 +405,11 @@
 @frappe.whitelist()
 def make_purchase_invoice(source_name, target_doc=None):
 	from frappe.model.mapper import get_mapped_doc
+	doc = frappe.get_doc('Purchase Receipt', source_name)
+	purchase_orders = [d.purchase_order for d in doc.items]
+	returned_qty_map_against_po = get_returned_qty_map_against_po(purchase_orders)
+	returned_qty_map_against_pr = get_returned_qty_map_against_pr(source_name)
+
 	invoiced_qty_map = get_invoiced_qty_map(source_name)
 
 	def set_missing_values(source, target):
@@ -417,7 +422,23 @@
 		doc.run_method("calculate_taxes_and_totals")
 
 	def update_item(source_doc, target_doc, source_parent):
-		target_doc.qty = source_doc.qty - invoiced_qty_map.get(source_doc.name, 0)
+		target_doc.qty, returned_qty = get_pending_qty(source_doc)
+		if not source_doc.purchase_order_item:
+			returned_qty_map_against_pr[source_doc.item_code] = returned_qty
+
+	def get_pending_qty(item_row):
+		pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) \
+			- returned_qty_map_against_po.get(item_row.purchase_order_item, 0)
+		returned_qty = flt(returned_qty_map_against_pr.get(item_row.item_code, 0))
+		if not item_row.purchase_order_item:
+			if returned_qty >= pending_qty:
+				pending_qty = 0
+				returned_qty -= pending_qty
+			else:
+				pending_qty -= returned_qty
+				returned_qty = 0
+		return pending_qty, returned_qty
+
 
 	doclist = get_mapped_doc("Purchase Receipt", source_name,	{
 		"Purchase Receipt": {
@@ -440,7 +461,7 @@
 				"asset": "asset",
 			},
 			"postprocess": update_item,
-			"filter": lambda d: abs(d.qty) - abs(invoiced_qty_map.get(d.name, 0))<=0
+			"filter": lambda d: get_pending_qty(d)[0]<=0
 		},
 		"Purchase Taxes and Charges": {
 			"doctype": "Purchase Taxes and Charges",
@@ -462,6 +483,31 @@
 
 	return invoiced_qty_map
 
+def get_returned_qty_map_against_po(purchase_orders):
+	"""returns a map: {so_detail: returned_qty}"""
+	returned_qty_map = {}
+
+	for name, returned_qty in frappe.get_all('Purchase Order Item', fields = ["name", "returned_qty"],
+		filters = {'parent': ('in', purchase_orders), 'docstatus': 1}, as_list=1):
+		if not returned_qty_map.get(name):
+				returned_qty_map[name] = 0
+		returned_qty_map[name] += returned_qty
+
+	return returned_qty_map
+
+def get_returned_qty_map_against_pr(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
+		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
+
 @frappe.whitelist()
 def make_purchase_return(source_name, target_doc=None):
 	from erpnext.controllers.sales_and_purchase_return import make_return_doc
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 472083b..c5ae838 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -404,6 +404,44 @@
 
 		set_perpetual_inventory(0, pr.company)
 
+	def test_make_purchase_invoice_from_pr_for_returned_qty(self):
+		from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order, create_pr_against_po
+
+		po = create_purchase_order()
+		pr = create_pr_against_po(po.name)
+
+		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.submit()
+
+		pi = make_purchase_invoice(pr.name)
+		self.assertEquals(pi.items[0].qty, 3)
+
+	def test_make_purchase_invoice_from_dn_with_returned_qty_against_dn(self):
+		pr1 = make_purchase_receipt(qty=8, do_not_submit=True)
+		pr1.append("items", {
+			"item_code": "_Test Item",
+			"warehouse": "_Test Warehouse - _TC",
+			"qty": 1,
+			"received_qty": 1,
+			"rate": 100,
+			"conversion_factor": 1.0,
+		})
+		pr1.submit()
+
+		pi1 = make_purchase_invoice(pr1.name)
+		pi1.items[0].qty = 4
+		pi1.items.pop(1)
+		pi1.save()
+		pi1.submit()
+
+		make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2)
+
+		pi2 = make_purchase_invoice(pr1.name)
+		self.assertEquals(pi2.items[0].qty, 2)
+		self.assertEquals(pi2.items[1].qty, 1)
+
 def get_gl_entries(voucher_type, voucher_no):
 	return frappe.db.sql("""select account, debit, credit, cost_center
 		from `tabGL Entry` where voucher_type=%s and voucher_no=%s