Merge branch 'develop' into provision-to-return-components
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index a53c42c..804f03d 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -17,6 +17,7 @@
 	close_work_order,
 	make_job_card,
 	make_stock_entry,
+	make_stock_return_entry,
 	stop_unstop,
 )
 from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
@@ -1408,6 +1409,77 @@
 		)
 		self.assertEqual(manufacture_ste_doc2.items[1].qty, 1)
 
+	def test_non_consumed_material_return_against_work_order(self):
+		frappe.db.set_value(
+			"Manufacturing Settings",
+			None,
+			"backflush_raw_materials_based_on",
+			"Material Transferred for Manufacture",
+		)
+
+		item = make_item(
+			"Test FG Item To Test Return Case",
+			{
+				"is_stock_item": 1,
+			},
+		)
+
+		item_code = item.name
+		bom_doc = make_bom(
+			item=item_code,
+			source_warehouse="Stores - _TC",
+			raw_materials=["Test Batch MCC Keyboard", "Test Serial No BTT Headphone"],
+		)
+
+		# Create a work order
+		wo_doc = make_wo_order_test_record(production_item=item_code, qty=5)
+		wo_doc.save()
+
+		self.assertEqual(wo_doc.bom_no, bom_doc.name)
+
+		# Transfer material for manufacture
+		ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 5))
+		for row in ste_doc.items:
+			row.qty += 2
+			row.transfer_qty += 2
+			nste_doc = test_stock_entry.make_stock_entry(
+				item_code=row.item_code, target="Stores - _TC", qty=row.qty, basic_rate=100
+			)
+
+			row.batch_no = nste_doc.items[0].batch_no
+			row.serial_no = nste_doc.items[0].serial_no
+
+		ste_doc.save()
+		ste_doc.submit()
+		ste_doc.load_from_db()
+
+		# Create a stock entry to manufacture the item
+		ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 5))
+		for row in ste_doc.items:
+			if row.s_warehouse and not row.t_warehouse:
+				row.qty -= 2
+				row.transfer_qty -= 2
+
+				if row.serial_no:
+					serial_nos = get_serial_nos(row.serial_no)
+					row.serial_no = "\n".join(serial_nos[0:5])
+
+		ste_doc.save()
+		ste_doc.submit()
+
+		wo_doc.load_from_db()
+		for row in wo_doc.required_items:
+			self.assertEqual(row.transferred_qty, 7)
+			self.assertEqual(row.consumed_qty, 5)
+
+		self.assertEqual(wo_doc.status, "Completed")
+		return_ste_doc = make_stock_return_entry(wo_doc.name)
+		return_ste_doc.save()
+
+		self.assertTrue(return_ste_doc.is_return)
+		for row in return_ste_doc.items:
+			self.assertEqual(row.qty, 2)
+
 
 def prepare_data_for_backflush_based_on_materials_transferred():
 	batch_item_doc = make_item(
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index f3640b9..4aab3fa 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -180,6 +180,37 @@
 				frm.trigger("make_bom");
 			});
 		}
+
+		frm.trigger("add_custom_button_to_return_components");
+	},
+
+	add_custom_button_to_return_components: function(frm) {
+		if (frm.doc.docstatus === 1 && in_list(["Closed", "Completed"], frm.doc.status)) {
+			let non_consumed_items = frm.doc.required_items.filter(d =>{
+				return flt(d.consumed_qty) < flt(d.transferred_qty - d.returned_qty)
+			});
+
+			if (non_consumed_items && non_consumed_items.length) {
+				frm.add_custom_button(__("Return Components"), function() {
+					frm.trigger("create_stock_return_entry");
+				}).addClass("btn-primary");
+			}
+		}
+	},
+
+	create_stock_return_entry: function(frm) {
+		frappe.call({
+			method: "erpnext.manufacturing.doctype.work_order.work_order.make_stock_return_entry",
+			args: {
+				"work_order": frm.doc.name,
+			},
+			callback: function(r) {
+				if(!r.exc) {
+					let doc = frappe.model.sync(r.message);
+					frappe.set_route("Form", doc[0].doctype, doc[0].name);
+				}
+			}
+		});
 	},
 
 	make_job_card: function(frm) {
@@ -517,7 +548,8 @@
 erpnext.work_order = {
 	set_custom_buttons: function(frm) {
 		var doc = frm.doc;
-		if (doc.docstatus === 1 && doc.status != "Closed") {
+
+		if (doc.status !== "Closed") {
 			frm.add_custom_button(__('Close'), function() {
 				frappe.confirm(__("Once the Work Order is Closed. It can't be resumed."),
 					() => {
@@ -525,7 +557,9 @@
 					}
 				);
 			}, __("Status"));
+		}
 
+		if (doc.docstatus === 1 && !in_list(["Closed", "Completed"], doc.status)) {
 			if (doc.status != 'Stopped' && doc.status != 'Completed') {
 				frm.add_custom_button(__('Stop'), function() {
 					erpnext.work_order.change_work_order_status(frm, "Stopped");
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 7b86253..1e6d982 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -20,6 +20,7 @@
 	nowdate,
 	time_diff_in_hours,
 )
+from pypika import functions as fn
 
 from erpnext.manufacturing.doctype.bom.bom import (
 	get_bom_item_rate,
@@ -859,6 +860,7 @@
 		if self.docstatus == 1:
 			# calculate transferred qty based on submitted stock entries
 			self.update_transferred_qty_for_required_items()
+			self.update_returned_qty()
 
 			# update in bin
 			self.update_reserved_qty_for_production()
@@ -930,23 +932,62 @@
 			self.set_available_qty()
 
 	def update_transferred_qty_for_required_items(self):
-		"""update transferred qty from submitted stock entries for that item against
-		the work order"""
+		ste = frappe.qb.DocType("Stock Entry")
+		ste_child = frappe.qb.DocType("Stock Entry Detail")
 
-		for d in self.required_items:
-			transferred_qty = frappe.db.sql(
-				"""select sum(qty)
-				from `tabStock Entry` entry, `tabStock Entry Detail` detail
-				where
-					entry.work_order = %(name)s
-					and entry.purpose = 'Material Transfer for Manufacture'
-					and entry.docstatus = 1
-					and detail.parent = entry.name
-					and (detail.item_code = %(item)s or detail.original_item = %(item)s)""",
-				{"name": self.name, "item": d.item_code},
-			)[0][0]
+		query = (
+			frappe.qb.from_(ste)
+			.inner_join(ste_child)
+			.on((ste_child.parent == ste.name))
+			.select(
+				ste_child.item_code,
+				ste_child.original_item,
+				fn.Sum(ste_child.qty).as_("qty"),
+			)
+			.where(
+				(ste.docstatus == 1)
+				& (ste.work_order == self.name)
+				& (ste.purpose == "Material Transfer for Manufacture")
+				& (ste.is_return == 0)
+			)
+			.groupby(ste_child.item_code)
+		)
 
-			d.db_set("transferred_qty", flt(transferred_qty), update_modified=False)
+		data = query.run(as_dict=1) or []
+		transferred_items = frappe._dict({d.original_item or d.item_code: d.qty for d in data})
+
+		for row in self.required_items:
+			row.db_set(
+				"transferred_qty", (transferred_items.get(row.item_code) or 0.0), update_modified=False
+			)
+
+	def update_returned_qty(self):
+		ste = frappe.qb.DocType("Stock Entry")
+		ste_child = frappe.qb.DocType("Stock Entry Detail")
+
+		query = (
+			frappe.qb.from_(ste)
+			.inner_join(ste_child)
+			.on((ste_child.parent == ste.name))
+			.select(
+				ste_child.item_code,
+				ste_child.original_item,
+				fn.Sum(ste_child.qty).as_("qty"),
+			)
+			.where(
+				(ste.docstatus == 1)
+				& (ste.work_order == self.name)
+				& (ste.purpose == "Material Transfer for Manufacture")
+				& (ste.is_return == 1)
+			)
+			.groupby(ste_child.item_code)
+		)
+
+		data = query.run(as_dict=1) or []
+		returned_dict = frappe._dict({d.original_item or d.item_code: d.qty for d in data})
+
+		for row in self.required_items:
+			row.db_set("returned_qty", (returned_dict.get(row.item_code) or 0.0), update_modified=False)
 
 	def update_consumed_qty_for_required_items(self):
 		"""
@@ -1470,3 +1511,25 @@
 			)
 		)
 	).run()[0][0] or 0.0
+
+
+@frappe.whitelist()
+def make_stock_return_entry(work_order):
+	from erpnext.stock.doctype.stock_entry.stock_entry import get_available_materials
+
+	non_consumed_items = get_available_materials(work_order)
+	if not non_consumed_items:
+		return
+
+	wo_doc = frappe.get_cached_doc("Work Order", work_order)
+
+	stock_entry = frappe.new_doc("Stock Entry")
+	stock_entry.from_bom = 1
+	stock_entry.is_return = 1
+	stock_entry.work_order = work_order
+	stock_entry.purpose = "Material Transfer for Manufacture"
+	stock_entry.bom_no = wo_doc.bom_no
+	stock_entry.add_transfered_raw_materials_in_items()
+	stock_entry.set_stock_entry_type()
+
+	return stock_entry
diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json
index 3acf572..f354d45 100644
--- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json
+++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json
@@ -20,6 +20,7 @@
   "column_break_11",
   "transferred_qty",
   "consumed_qty",
+  "returned_qty",
   "available_qty_at_source_warehouse",
   "available_qty_at_wip_warehouse"
  ],
@@ -97,6 +98,7 @@
    "fieldtype": "Column Break"
   },
   {
+   "columns": 1,
    "depends_on": "eval:!parent.skip_transfer",
    "fieldname": "consumed_qty",
    "fieldtype": "Float",
@@ -127,11 +129,19 @@
    "fieldtype": "Currency",
    "label": "Amount",
    "read_only": 1
+  },
+  {
+   "columns": 1,
+   "fieldname": "returned_qty",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Returned Qty ",
+   "read_only": 1
   }
  ],
  "istable": 1,
  "links": [],
- "modified": "2020-04-13 18:46:32.966416",
+ "modified": "2022-09-28 10:50:43.512562",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Work Order Item",
@@ -140,5 +150,6 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js
index b2428e8..2fb4ec6 100644
--- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js
@@ -50,7 +50,7 @@
 			label: __("Status"),
 			fieldname: "status",
 			fieldtype: "Select",
-			options: ["In Process", "Completed", "Stopped"]
+			options: ["", "In Process", "Completed", "Stopped"]
 		},
 		{
 			label: __("Excess Materials Consumed"),
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py
index 8158bc9..14e97d3 100644
--- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py
@@ -1,6 +1,8 @@
 # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
 
+from collections import defaultdict
+
 import frappe
 from frappe import _
 
@@ -18,7 +20,11 @@
 	filters = get_filter_condition(report_filters)
 
 	wo_items = {}
-	for d in frappe.get_all("Work Order", filters=filters, fields=fields):
+
+	work_orders = frappe.get_all("Work Order", filters=filters, fields=fields)
+	returned_materials = get_returned_materials(work_orders)
+
+	for d in work_orders:
 		d.extra_consumed_qty = 0.0
 		if d.consumed_qty and d.consumed_qty > d.required_qty:
 			d.extra_consumed_qty = d.consumed_qty - d.required_qty
@@ -39,6 +45,28 @@
 	return data
 
 
+def get_returned_materials(work_orders):
+	raw_materials_qty = defaultdict(float)
+
+	raw_materials = frappe.get_all(
+		"Stock Entry",
+		fields=["`tabStock Entry Detail`.`item_code`", "`tabStock Entry Detail`.`qty`"],
+		filters=[
+			["Stock Entry", "is_return", "=", 1],
+			["Stock Entry Detail", "docstatus", "=", 1],
+			["Stock Entry", "work_order", "in", [d.name for d in work_orders]],
+		],
+	)
+
+	for d in raw_materials:
+		raw_materials_qty[d.item_code] += d.qty
+
+	for row in work_orders:
+		row.returned_qty = 0.0
+		if raw_materials_qty.get(row.raw_material_item_code):
+			row.returned_qty = raw_materials_qty.get(row.raw_material_item_code)
+
+
 def get_fields():
 	return [
 		"`tabWork Order Item`.`parent`",
@@ -65,7 +93,7 @@
 	for field in ["name", "production_item", "company", "status"]:
 		value = report_filters.get(field)
 		if value:
-			key = f"`{field}`"
+			key = f"{field}"
 			filters.update({key: value})
 
 	return filters
@@ -112,4 +140,10 @@
 			"fieldtype": "Float",
 			"width": 100,
 		},
+		{
+			"label": _("Returned Qty"),
+			"fieldname": "returned_qty",
+			"fieldtype": "Float",
+			"width": 100,
+		},
 	]
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index abe98e2..7e9420d 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -148,19 +148,19 @@
    "search_index": 1
   },
   {
-    "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"",
-    "fieldname": "purchase_order",
-    "fieldtype": "Link",
-    "label": "Purchase Order",
-    "options": "Purchase Order"
+   "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"",
+   "fieldname": "purchase_order",
+   "fieldtype": "Link",
+   "label": "Purchase Order",
+   "options": "Purchase Order"
   },
   {
-    "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"",
-    "fieldname": "subcontracting_order",
-    "fieldtype": "Link",
-    "label": "Subcontracting Order",
-    "options": "Subcontracting Order"
-   },
+   "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"",
+   "fieldname": "subcontracting_order",
+   "fieldtype": "Link",
+   "label": "Subcontracting Order",
+   "options": "Subcontracting Order"
+  },
   {
    "depends_on": "eval:doc.purpose==\"Sales Return\"",
    "fieldname": "delivery_note_no",
@@ -616,6 +616,7 @@
    "fieldname": "is_return",
    "fieldtype": "Check",
    "hidden": 1,
+   "in_list_view": 1,
    "label": "Is Return",
    "no_copy": 1,
    "print_hide": 1,
@@ -627,7 +628,7 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-05-02 05:21:39.060501",
+ "modified": "2022-10-07 14:39:51.943770",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Entry",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 8bcd772..b116735 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -1212,13 +1212,19 @@
 
 	def update_work_order(self):
 		def _validate_work_order(pro_doc):
+			msg, title = "", ""
 			if flt(pro_doc.docstatus) != 1:
-				frappe.throw(_("Work Order {0} must be submitted").format(self.work_order))
+				msg = f"Work Order {self.work_order} must be submitted"
 
 			if pro_doc.status == "Stopped":
-				frappe.throw(
-					_("Transaction not allowed against stopped Work Order {0}").format(self.work_order)
-				)
+				msg = f"Transaction not allowed against stopped Work Order {self.work_order}"
+
+			if self.is_return and pro_doc.status not in ["Completed", "Closed"]:
+				title = _("Stock Return")
+				msg = f"Work Order {self.work_order} must be completed or closed"
+
+			if msg:
+				frappe.throw(_(msg), title=title)
 
 		if self.job_card:
 			job_doc = frappe.get_doc("Job Card", self.job_card)
@@ -1754,10 +1760,12 @@
 
 		for key, row in available_materials.items():
 			remaining_qty_to_produce = flt(wo_data.trans_qty) - flt(wo_data.produced_qty)
-			if remaining_qty_to_produce <= 0:
+			if remaining_qty_to_produce <= 0 and not self.is_return:
 				continue
 
-			qty = (flt(row.qty) * flt(self.fg_completed_qty)) / remaining_qty_to_produce
+			qty = flt(row.qty)
+			if not self.is_return:
+				qty = (flt(row.qty) * flt(self.fg_completed_qty)) / remaining_qty_to_produce
 
 			item = row.item_details
 			if cint(frappe.get_cached_value("UOM", item.stock_uom, "must_be_whole_number")):
@@ -1781,6 +1789,9 @@
 				self.update_item_in_stock_entry_detail(row, item, qty)
 
 	def update_item_in_stock_entry_detail(self, row, item, qty) -> None:
+		if not qty:
+			return
+
 		ste_item_details = {
 			"from_warehouse": item.warehouse,
 			"to_warehouse": "",
@@ -1794,6 +1805,9 @@
 			"original_item": item.original_item,
 		}
 
+		if self.is_return:
+			ste_item_details["to_warehouse"] = item.s_warehouse
+
 		if row.serial_nos:
 			serial_nos = row.serial_nos
 			if item.batch_no:
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_list.js b/erpnext/stock/doctype/stock_entry/stock_entry_list.js
index cbc3491..4eb0da1 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_list.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_list.js
@@ -1,8 +1,13 @@
 frappe.listview_settings['Stock Entry'] = {
 	add_fields: ["`tabStock Entry`.`from_warehouse`", "`tabStock Entry`.`to_warehouse`",
-		"`tabStock Entry`.`purpose`", "`tabStock Entry`.`work_order`", "`tabStock Entry`.`bom_no`"],
+		"`tabStock Entry`.`purpose`", "`tabStock Entry`.`work_order`", "`tabStock Entry`.`bom_no`",
+		"`tabStock Entry`.`is_return`"],
 	get_indicator: function (doc) {
-		if (doc.docstatus === 0) {
+		debugger
+		if(doc.is_return===1 && doc.purpose === "Material Transfer for Manufacture") {
+			return [__("Material Returned from WIP"), "orange",
+				"is_return,=,1|purpose,=,Material Transfer for Manufacture|docstatus,<,2"];
+		} else if (doc.docstatus === 0) {
 			return [__("Draft"), "red", "docstatus,=,0"];
 
 		} else if (doc.purpose === 'Send to Warehouse' && doc.per_transferred < 100) {