fix: Status updater fixes in PO and SO and test coverage for return (#17491)

* fix: Status updator fixes in sales and purchase cycle

* fix: Test cases for return in so and po

* fix: Resolve conflicts
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 95d49a4..97ad0ea 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -100,6 +100,7 @@
 		self.validate_fixed_asset()
 		self.create_remarks()
 		self.set_status()
+		self.validate_purchase_receipt_if_update_stock()
 		validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
 
 	def validate_release_date(self):
@@ -284,7 +285,7 @@
 
 	def update_status_updater_args(self):
 		if cint(self.update_stock):
-			self.status_updater.extend([{
+			self.status_updater.append({
 				'source_dt': 'Purchase Invoice Item',
 				'target_dt': 'Purchase Order Item',
 				'join_field': 'po_detail',
@@ -292,28 +293,29 @@
 				'target_parent_dt': 'Purchase Order',
 				'target_parent_field': 'per_received',
 				'target_ref_field': 'qty',
-				'source_field': 'qty',
+				'source_field': 'received_qty',
+				'second_source_dt': 'Purchase Receipt Item',
+				'second_source_field': 'received_qty',
+				'second_join_field': 'purchase_order_item',
 				'percent_join_field':'purchase_order',
-				# 'percent_join_field': 'prevdoc_docname',
 				'overflow_type': 'receipt',
 				'extra_cond': """ and exists(select name from `tabPurchase Invoice`
 					where name=`tabPurchase Invoice Item`.parent and update_stock = 1)"""
-			},
-			{
-				'source_dt': 'Purchase Invoice Item',
-				'target_dt': 'Purchase Order Item',
-				'join_field': 'po_detail',
-				'target_field': 'returned_qty',
-				'target_parent_dt': 'Purchase Order',
-				# 'target_parent_field': 'per_received',
-				# 'target_ref_field': 'qty',
-				'source_field': '-1 * qty',
-				# 'percent_join_field': 'prevdoc_docname',
-				# 'overflow_type': 'receipt',
-				'extra_cond': """ and exists (select name from `tabPurchase Invoice`
-					where name=`tabPurchase Invoice Item`.parent and update_stock=1 and is_return=1)"""
-			}
-		])
+			})
+			if cint(self.is_return):
+				self.status_updater.append({
+					'source_dt': 'Purchase Invoice Item',
+					'target_dt': 'Purchase Order Item',
+					'join_field': 'po_detail',
+					'target_field': 'returned_qty',
+					'source_field': '-1 * qty',
+					'second_source_dt': 'Purchase Receipt Item',
+					'second_source_field': '-1 * qty',
+					'second_join_field': 'purchase_order_item',
+					'overflow_type': 'receipt',
+					'extra_cond': """ and exists (select name from `tabPurchase Invoice`
+						where name=`tabPurchase Invoice Item`.parent and update_stock=1 and is_return=1)"""
+				})
 
 	def validate_purchase_receipt_if_update_stock(self):
 		if self.update_stock:
@@ -327,13 +329,13 @@
 
 		self.check_prev_docstatus()
 		self.update_status_updater_args()
+		self.update_prevdoc_status()
 
 		frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype,
 			self.company, self.base_grand_total)
 
 		if not self.is_return:
 			self.update_against_document_in_jv()
-			self.update_prevdoc_status()
 			self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
 			self.update_billing_status_in_pr()
 
@@ -763,9 +765,9 @@
 		self.check_on_hold_or_closed_status()
 
 		self.update_status_updater_args()
+		self.update_prevdoc_status()
 
 		if not self.is_return:
-			self.update_prevdoc_status()
 			self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
 			self.update_billing_status_in_pr()
 
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 06e1e58..dd5f8fd 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -254,7 +254,7 @@
 
 	def update_status_updater_args(self):
 		if cint(self.update_stock):
-			self.status_updater.extend([{
+			self.status_updater.append({
 				'source_dt':'Sales Invoice Item',
 				'target_dt':'Sales Order Item',
 				'target_parent_dt':'Sales Order',
@@ -272,21 +272,20 @@
 				'overflow_type': 'delivery',
 				'extra_cond': """ and exists(select name from `tabSales Invoice`
 					where name=`tabSales Invoice Item`.parent and update_stock = 1)"""
-			},
-			{
-				'source_dt': 'Sales Invoice Item',
-				'target_dt': 'Sales Order Item',
-				'join_field': 'so_detail',
-				'target_field': 'returned_qty',
-				'target_parent_dt': 'Sales Order',
-				# 'target_parent_field': 'per_delivered',
-				# 'target_ref_field': 'qty',
-				'source_field': '-1 * qty',
-				# 'percent_join_field': 'sales_order',
-				# 'overflow_type': 'delivery',
-				'extra_cond': """ and exists (select name from `tabSales Invoice` where name=`tabSales Invoice Item`.parent and update_stock=1 and is_return=1)"""
-			}
-		])
+			})
+			if cint(self.is_return):
+				self.status_updater.append({
+					'source_dt': 'Sales Invoice Item',
+					'target_dt': 'Sales Order Item',
+					'join_field': 'so_detail',
+					'target_field': 'returned_qty',
+					'target_parent_dt': 'Sales Order',
+					'source_field': '-1 * qty',
+					'second_source_dt': 'Delivery Note Item',
+					'second_source_field': '-1 * qty',
+					'second_join_field': 'so_detail',
+					'extra_cond': """ and exists (select name from `tabSales Invoice` where name=`tabSales Invoice Item`.parent and update_stock=1 and is_return=1)"""
+				})
 
 	def check_credit_limit(self):
 		from erpnext.selling.doctype.customer.customer import check_credit_limit
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 812aad8..820c93c 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -108,6 +108,69 @@
 		self.assertEqual(po.get("items")[0].amount, 1400)
 		self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3)
 
+	def test_update_qty(self):
+		po = create_purchase_order()
+
+		make_pr_against_po(po.name, 6)
+
+		po.load_from_db()
+		self.assertEqual(po.get("items")[0].received_qty, 6)
+
+		# Check received_qty after make_purchase_invoice without update_stock checked
+		pi1 = make_purchase_invoice(po.name)
+		pi1.get("items")[0].qty = 6
+		pi1.insert()
+		pi1.submit()
+
+		po.load_from_db()
+		self.assertEqual(po.get("items")[0].received_qty, 6)
+
+		# Check received_qty after make_purchase_invoice with update_stock checked
+		pi2 = make_purchase_invoice(po.name)
+		pi2.set("update_stock", 1)
+		pi2.get("items")[0].qty = 3
+		pi2.insert()
+		pi2.submit()
+
+		po.load_from_db()
+		self.assertEqual(po.get("items")[0].received_qty, 9)
+
+	def test_return_against_purchase_order(self):
+		po = create_purchase_order()
+
+		pr = make_pr_against_po(po.name, 6)
+
+		po.load_from_db()
+		self.assertEqual(po.get("items")[0].received_qty, 6)
+
+		pi2 = make_purchase_invoice(po.name)
+		pi2.set("update_stock", 1)
+		pi2.get("items")[0].qty = 3
+		pi2.insert()
+		pi2.submit()
+
+		po.load_from_db()
+		self.assertEqual(po.get("items")[0].received_qty, 9)
+
+		# Make return purchase receipt, purchase invoice and check quantity
+		from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
+				import make_purchase_receipt as make_purchase_receipt_return
+		from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice \
+				import make_purchase_invoice as make_purchase_invoice_return
+
+		pr1 = make_purchase_receipt_return(is_return=1, return_against=pr.name, qty=-3, do_not_submit=True)
+		pr1.items[0].purchase_order = po.name
+		pr1.items[0].purchase_order_item = po.items[0].name
+		pr1.submit()
+
+		pi1= make_purchase_invoice_return(is_return=1, return_against=pi2.name, qty=-1, update_stock=1, do_not_submit=True)
+		pi1.items[0].purchase_order = po.name
+		pi1.items[0].po_detail = po.items[0].name
+		pi1.submit()
+
+
+		po.load_from_db()
+		self.assertEqual(po.get("items")[0].received_qty, 5)
 
 	def test_make_purchase_invoice(self):
 		po = create_purchase_order(do_not_submit=True)
@@ -510,6 +573,13 @@
 		frappe.db.set_value("Accounts Settings", "Accounts Settings",
 			"unlink_advance_payment_on_cancelation_of_order", 0)
 
+def make_pr_against_po(po, received_qty=0):
+	pr = make_purchase_receipt(po)
+	pr.get("items")[0].qty = received_qty or 5
+	pr.insert()
+	pr.submit()
+	return pr
+
 def make_subcontracted_item(item_code):
 	from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
 
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 4fca15b..e7697e2 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -124,6 +124,44 @@
 		so.load_from_db()
 		self.assertEqual(so.get("items")[0].delivered_qty, 9)
 
+	def test_return_against_sales_order(self):
+		so = make_sales_order()
+
+		dn = create_dn_against_so(so.name, 6)
+
+		so.load_from_db()
+		self.assertEqual(so.get("items")[0].delivered_qty, 6)
+
+		# Check delivered_qty after make_sales_invoice with update_stock checked
+		si2 = make_sales_invoice(so.name)
+		si2.set("update_stock", 1)
+		si2.get("items")[0].qty = 3
+		si2.insert()
+		si2.submit()
+
+		so.load_from_db()
+
+		self.assertEqual(so.get("items")[0].delivered_qty, 9)
+
+		# Make return deliver note, sales invoice and check quantity
+		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+		from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
+		dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3, do_not_submit=True)
+		dn1.items[0].against_sales_order = so.name
+		dn1.items[0].so_detail = so.items[0].name
+		dn1.submit()
+
+		si1 = create_sales_invoice(is_return=1, return_against=si2.name, qty=-1, update_stock=1, do_not_submit=True)
+		si1.items[0].sales_order = so.name
+		si1.items[0].so_detail = so.items[0].name
+		si1.submit()
+
+
+		so.load_from_db()
+		self.assertEqual(so.get("items")[0].delivered_qty, 5)
+
+
 	def test_reserved_qty_for_partial_delivery(self):
 		make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100)
 		existing_reserved_qty = get_reserved_qty()
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index b8c4604..1e1030c 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -52,16 +52,20 @@
 			'percent_join_field': 'against_sales_invoice',
 			'overflow_type': 'delivery',
 			'no_tolerance': 1
-		},
-		{
-			'source_dt': 'Delivery Note Item',
-			'target_dt': 'Sales Order Item',
-			'join_field': 'so_detail',
-			'target_field': 'returned_qty',
-			'target_parent_dt': 'Sales Order',
-			'source_field': '-1 * qty',
-			'extra_cond': """ and exists (select name from `tabDelivery Note` where name=`tabDelivery Note Item`.parent and is_return=1)"""
 		}]
+		if cint(self.is_return):
+			self.status_updater.append({
+				'source_dt': 'Delivery Note Item',
+				'target_dt': 'Sales Order Item',
+				'join_field': 'so_detail',
+				'target_field': 'returned_qty',
+				'target_parent_dt': 'Sales Order',
+				'source_field': '-1 * qty',
+				'second_source_dt': 'Sales Invoice Item',
+				'second_source_field': '-1 * qty',
+				'second_join_field': 'so_detail',
+				'extra_cond': """ and exists (select name from `tabDelivery Note` where name=`tabDelivery Note Item`.parent and is_return=1)"""
+			})
 
 	def before_print(self):
 		def toggle_print_hide(meta, fieldname):
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 1bd55f8..54a414c 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -24,14 +24,17 @@
 	def __init__(self, *args, **kwargs):
 		super(PurchaseReceipt, self).__init__(*args, **kwargs)
 		self.status_updater = [{
-			'source_dt': 'Purchase Receipt Item',
 			'target_dt': 'Purchase Order Item',
 			'join_field': 'purchase_order_item',
 			'target_field': 'received_qty',
 			'target_parent_dt': 'Purchase Order',
 			'target_parent_field': 'per_received',
 			'target_ref_field': 'qty',
-			'source_field': 'qty',
+			'source_dt': 'Purchase Receipt Item',
+			'source_field': 'received_qty',
+			'second_source_dt': 'Purchase Invoice Item',
+			'second_source_field': 'received_qty',
+			'second_join_field': 'po_detail',
 			'percent_join_field': 'purchase_order',
 			'overflow_type': 'receipt'
 		},
@@ -58,6 +61,18 @@
 			# 'overflow_type': 'receipt',
 			'extra_cond': """ and exists (select name from `tabPurchase Receipt` where name=`tabPurchase Receipt Item`.parent and is_return=1)"""
 		}]
+		if cint(self.is_return):
+			self.status_updater.append({
+				'source_dt': 'Purchase Receipt Item',
+				'target_dt': 'Purchase Order Item',
+				'join_field': 'purchase_order_item',
+				'target_field': 'returned_qty',
+				'source_field': '-1 * qty',
+				'second_source_dt': 'Purchase Invoice Item',
+				'second_source_field': '-1 * qty',
+				'second_join_field': 'po_detail',
+				'extra_cond': """ and exists (select name from `tabPurchase Receipt` where name=`tabPurchase Receipt Item`.parent and is_return=1)"""
+			})
 
 	def validate(self):
 		self.validate_posting_time()