Merge branch 'develop' into feat-bom-process-loss-fp
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index e72c8eb..1481f1c 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -446,6 +446,11 @@
 			},
 			callback: function(r) {
 				d = locals[cdt][cdn];
+				if (d.is_process_loss) {
+					r.message.rate = 0;
+					r.message.base_rate = 0;
+				}
+
 				$.extend(d, r.message);
 				refresh_field("items");
 				refresh_field("scrap_items");
@@ -655,3 +660,35 @@
 		frm.set_value("operations", []);
 	}
 });
+
+frappe.ui.form.on("BOM Scrap Item", {
+	item_code(frm, cdt, cdn) {
+		const { item_code } = locals[cdt][cdn];
+		if (item_code === frm.doc.item) {
+			locals[cdt][cdn].is_process_loss = 1;
+			trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code);
+		}
+	},
+});
+
+function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) {
+	frappe.prompt(
+		{
+			fieldname: "percent",
+			fieldtype: "Percent",
+			label: __("% Finished Item Quantity"),
+			description:
+				__("Set quantity of process loss item:") +
+				` ${item_code} ` +
+				__("as a percentage of finished item quantity"),
+		},
+		(data) => {
+			const row = locals[cdt][cdn];
+			row.stock_qty = (frm.doc.quantity * data.percent) / 100;
+			row.qty = row.stock_qty / (row.conversion_factor || 1);
+			refresh_field("scrap_items");
+		},
+		__("Set Process Loss Item Quantity"),
+		__("Set Quantity")
+	);
+}
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 0ba8507..8d10578 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -155,6 +155,7 @@
 		self.update_stock_qty()
 		self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
 		self.set_bom_level()
+		self.validate_scrap_items()
 
 	def get_context(self, context):
 		context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
@@ -691,6 +692,32 @@
 		if update:
 			self.db_set("bom_level", self.bom_level)
 
+	def validate_scrap_items(self):
+		for item in self.scrap_items:
+			msg = ""
+			if item.item_code == self.item and not item.is_process_loss:
+				msg = _('Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked.') \
+					.format(frappe.bold(item.item_code))
+			elif item.item_code != self.item and item.is_process_loss:
+				msg = _('Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from  the item to be manufactured or repacked') \
+					.format(frappe.bold(item.item_code))
+
+			must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number")
+			if item.is_process_loss and must_be_whole_number:
+				msg = _("Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM.") \
+					.format(frappe.bold(item.item_code), frappe.bold(item.stock_uom))
+
+			if item.is_process_loss and (item.stock_qty >= self.quantity):
+				msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.") \
+					.format(frappe.bold(item.item_code))
+
+			if item.is_process_loss and (item.rate > 0):
+				msg = _("Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked.") \
+					.format(frappe.bold(item.item_code))
+
+			if msg:
+				frappe.throw(msg, title=_("Note"))
+
 def get_bom_item_rate(args, bom_doc):
 	if bom_doc.rm_cost_as_per == 'Valuation Rate':
 		rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)
@@ -808,8 +835,11 @@
 
 		items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True)
 	elif fetch_scrap_items:
-		query = query.format(table="BOM Scrap Item", where_conditions="",
-			select_columns=", bom_item.idx, item.description", is_stock_item=is_stock_item, qty_field="stock_qty")
+		query = query.format(
+			table="BOM Scrap Item", where_conditions="",
+			select_columns=", bom_item.idx, item.description, is_process_loss",
+			is_stock_item=is_stock_item, qty_field="stock_qty"
+		)
 
 		items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True)
 	else:
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index c89f7d6..6e17f2a 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -280,6 +280,38 @@
 			self.assertEqual(reqd_item.qty, created_item.qty)
 			self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
 
+	def test_bom_with_process_loss_item(self):
+		fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
+
+		if not frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001"):
+			bom_doc = create_bom_with_process_loss_item(
+				fg_item_non_whole, bom_item, 0.25, 0, 1
+			)
+			bom_doc.submit()
+
+		bom_doc = create_bom_with_process_loss_item(
+			fg_item_non_whole, bom_item, 2, 0
+		)
+		#  PL Item qty can't be >= FG Item qty
+		self.assertRaises(frappe.ValidationError, bom_doc.submit)
+
+		bom_doc = create_bom_with_process_loss_item(
+			fg_item_non_whole, bom_item, 1, 100
+		)
+		# PL Item rate has to be 0
+		self.assertRaises(frappe.ValidationError, bom_doc.submit)
+
+		bom_doc = create_bom_with_process_loss_item(
+			fg_item_whole, bom_item, 0.25, 0
+		)
+		#  Items with whole UOMs can't be PL Items
+		self.assertRaises(frappe.ValidationError, bom_doc.submit)
+
+		bom_doc = create_bom_with_process_loss_item(
+			fg_item_non_whole, bom_item, 0.25, 0, is_process_loss=0
+		)
+		# FG Items in Scrap/Loss Table should have Is Process Loss set
+		self.assertRaises(frappe.ValidationError, bom_doc.submit)
 
 def get_default_bom(item_code="_Test FG Item 2"):
 	return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
@@ -353,3 +385,44 @@
 
 	for warehouse in warehouse_list:
 		create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate)
+
+def create_bom_with_process_loss_item(
+		fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1):
+	bom_doc = frappe.new_doc("BOM")
+	bom_doc.item = fg_item.item_code
+	bom_doc.quantity = fg_qty
+	bom_doc.append("items", {
+		"item_code": bom_item.item_code,
+		"qty": 1,
+		"uom": bom_item.stock_uom,
+		"stock_uom": bom_item.stock_uom,
+		"rate": 100.0
+	})
+	bom_doc.append("scrap_items", {
+		"item_code": fg_item.item_code,
+		"qty": scrap_qty,
+		"stock_qty": scrap_qty,
+		"uom": fg_item.stock_uom,
+		"stock_uom": fg_item.stock_uom,
+		"rate": scrap_rate,
+		"is_process_loss": is_process_loss
+	})
+	return bom_doc
+
+def create_process_loss_bom_items():
+	item_list = [
+		("_Test Item - Non Whole UOM", "Kg"),
+		("_Test Item - Whole UOM", "Unit"),
+		("_Test PL BOM Item", "Unit")
+	]
+	return [create_process_loss_bom_item(it) for it in item_list]
+
+def create_process_loss_bom_item(item_tuple):
+	item_code, stock_uom = item_tuple
+	if frappe.db.exists("Item", item_code) is None:
+		return make_item(
+			item_code,
+			{'stock_uom':stock_uom, 'valuation_rate':100}
+		)
+	else:
+		return frappe.get_doc("Item", item_code)
diff --git a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json
index 9f7091d..7018082 100644
--- a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json
+++ b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json
@@ -1,345 +1,112 @@
 {
- "allow_copy": 0, 
- "allow_guest_to_view": 0, 
- "allow_import": 0, 
- "allow_rename": 0, 
- "beta": 0, 
- "creation": "2016-09-26 02:19:21.642081", 
- "custom": 0, 
- "docstatus": 0, 
- "doctype": "DocType", 
- "document_type": "", 
- "editable_grid": 1, 
+ "actions": [],
+ "creation": "2016-09-26 02:19:21.642081",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "item_code",
+  "column_break_2",
+  "item_name",
+  "is_process_loss",
+  "quantity_and_rate",
+  "stock_qty",
+  "rate",
+  "amount",
+  "column_break_6",
+  "stock_uom",
+  "base_rate",
+  "base_amount"
+ ],
  "fields": [
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "item_code", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Item Code", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "Item", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 1, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "item_code",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Item Code",
+   "options": "Item",
+   "reqd": 1
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "item_name", 
-   "fieldtype": "Data", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Item Name", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "item_name",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Item Name"
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "quantity_and_rate", 
-   "fieldtype": "Section Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Quantity and Rate", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "quantity_and_rate",
+   "fieldtype": "Section Break",
+   "label": "Quantity and Rate"
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "stock_qty", 
-   "fieldtype": "Float", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Qty", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 1, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "stock_qty",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Qty",
+   "reqd": 1
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "rate", 
-   "fieldtype": "Currency", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Rate", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "currency", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "rate",
+   "fieldtype": "Currency",
+   "in_list_view": 1,
+   "label": "Rate",
+   "options": "currency"
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "amount", 
-   "fieldtype": "Currency", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Amount", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "currency", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 1, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "amount",
+   "fieldtype": "Currency",
+   "label": "Amount",
+   "options": "currency",
+   "read_only": 1
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "column_break_6", 
-   "fieldtype": "Column Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "column_break_6",
+   "fieldtype": "Column Break"
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "stock_uom", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Stock UOM", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "UOM", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 1, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "stock_uom",
+   "fieldtype": "Link",
+   "label": "Stock UOM",
+   "options": "UOM",
+   "read_only": 1
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "base_rate", 
-   "fieldtype": "Currency", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Basic Rate (Company Currency)", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "Company:company:default_currency", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 1, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 1, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "base_rate",
+   "fieldtype": "Currency",
+   "label": "Basic Rate (Company Currency)",
+   "options": "Company:company:default_currency",
+   "print_hide": 1,
+   "read_only": 1
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "base_amount", 
-   "fieldtype": "Currency", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Basic Amount (Company Currency)", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "Company:company:default_currency", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 1, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 1, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
+   "fieldname": "base_amount",
+   "fieldtype": "Currency",
+   "label": "Basic Amount (Company Currency)",
+   "options": "Company:company:default_currency",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_2",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "is_process_loss",
+   "fieldtype": "Check",
+   "label": "Is Process Loss"
   }
- ], 
- "has_web_view": 0, 
- "hide_heading": 0, 
- "hide_toolbar": 0, 
- "idx": 0, 
- "image_view": 0, 
- "in_create": 0, 
- "is_submittable": 0, 
- "issingle": 0, 
- "istable": 1, 
- "max_attachments": 0, 
- "modified": "2017-07-04 16:04:32.442287", 
- "modified_by": "Administrator", 
- "module": "Manufacturing", 
- "name": "BOM Scrap Item", 
- "name_case": "", 
- "owner": "Administrator", 
- "permissions": [], 
- "quick_entry": 1, 
- "read_only": 0, 
- "read_only_onload": 0, 
- "show_name_in_global_search": 0, 
- "sort_field": "modified", 
- "sort_order": "DESC", 
- "track_changes": 1, 
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2021-06-22 16:46:12.153311",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "BOM Scrap Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index bf1ccb7..a00520f 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -690,6 +690,71 @@
 
 		self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture')
 
+	def test_wo_completion_with_pl_bom(self):
+		from erpnext.manufacturing.doctype.bom.test_bom import create_process_loss_bom_items
+		from erpnext.manufacturing.doctype.bom.test_bom import create_bom_with_process_loss_item
+
+		qty = 4
+		scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG
+		source_warehouse = "Stores - _TC"
+		wip_warehouse = "_Test Warehouse - _TC"
+		fg_item_non_whole, _, bom_item = create_process_loss_bom_items()
+
+		test_stock_entry.make_stock_entry(item_code=bom_item.item_code,
+			target=source_warehouse, qty=4, basic_rate=100)
+
+		bom_no = f"BOM-{fg_item_non_whole.item_code}-001"
+		if not frappe.db.exists("BOM", bom_no):
+			bom_doc = create_bom_with_process_loss_item(
+				fg_item_non_whole, bom_item, scrap_qty=scrap_qty,
+				scrap_rate=0, fg_qty=1, is_process_loss=1
+			)
+			bom_doc.submit()
+
+		wo = make_wo_order_test_record(
+			production_item=fg_item_non_whole.item_code,
+			bom_no=bom_no,
+			wip_warehouse=wip_warehouse,
+			qty=qty,
+			skip_transfer=1,
+			stock_uom=fg_item_non_whole.stock_uom,
+		)
+
+		se = frappe.get_doc(
+			make_stock_entry(wo.name, "Material Transfer for Manufacture", qty)
+		)
+		se.get("items")[0].s_warehouse = "Stores - _TC"
+		se.insert()
+		se.submit()
+
+		se = frappe.get_doc(
+			make_stock_entry(wo.name, "Manufacture", qty)
+		)
+		se.insert()
+		se.submit()
+
+		# Testing stock entry values
+		items = se.get("items")
+		self.assertEqual(len(items), 3, "There should be 3 items including process loss.")
+
+		source_item, fg_item, pl_item = items
+
+		total_pl_qty = qty * scrap_qty
+		actual_fg_qty = qty - total_pl_qty
+
+		self.assertEqual(pl_item.qty, total_pl_qty)
+		self.assertEqual(fg_item.qty, actual_fg_qty)
+
+		# Testing Work Order values
+		self.assertEqual(
+			frappe.db.get_value("Work Order", wo.name, "produced_qty"),
+			qty
+		)
+		self.assertEqual(
+			frappe.db.get_value("Work Order", wo.name, "process_loss_qty"),
+			actual_fg_qty
+		)
+
 def get_scrap_item_details(bom_no):
 	scrap_items = {}
 	for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index 3b56854..913fc85 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -19,6 +19,7 @@
   "qty",
   "material_transferred_for_manufacturing",
   "produced_qty",
+  "process_loss_qty",
   "sales_order",
   "project",
   "serial_no_and_batch_for_finished_good_section",
@@ -64,16 +65,12 @@
   "description",
   "stock_uom",
   "column_break2",
-  "references_section",
   "material_request",
   "material_request_item",
   "sales_order_item",
-  "column_break_61",
   "production_plan",
   "production_plan_item",
   "production_plan_sub_assembly_item",
-  "parent_work_order",
-  "bom_level",
   "product_bundle_item",
   "amended_from"
  ],
@@ -553,20 +550,29 @@
    "read_only": 1
   },
   {
-    "fieldname": "production_plan_sub_assembly_item",
-    "fieldtype": "Data",
-    "label": "Production Plan Sub-assembly Item",
-    "no_copy": 1,
-    "print_hide": 1,
-    "read_only": 1
-   }
+   "fieldname": "production_plan_sub_assembly_item",
+   "fieldtype": "Data",
+   "label": "Production Plan Sub-assembly Item",
+   "no_copy": 1,
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "depends_on": "eval: doc.process_loss_qty",
+   "fieldname": "process_loss_qty",
+   "fieldtype": "Float",
+   "label": "Process Loss Qty",
+   "no_copy": 1,
+   "non_negative": 1,
+   "read_only": 1
+  }
  ],
  "icon": "fa fa-cogs",
  "idx": 1,
  "image_field": "image",
  "is_submittable": 1,
  "links": [],
- "modified": "2021-06-28 16:19:14.902699",
+ "modified": "2021-08-24 15:14:03.844937",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Work Order",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 5fe9fec..24b33d5 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -214,6 +214,7 @@
 					self.meta.get_label(fieldname), qty, completed_qty, self.name), StockOverProductionError)
 
 			self.db_set(fieldname, qty)
+			self.set_process_loss_qty()
 
 			from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item
 
@@ -223,6 +224,22 @@
 		if self.production_plan:
 			self.update_production_plan_status()
 
+	def set_process_loss_qty(self):
+		process_loss_qty = flt(frappe.db.sql("""
+				SELECT sum(qty) FROM `tabStock Entry Detail`
+				WHERE
+					is_process_loss=1
+					AND parent IN (
+						SELECT name FROM `tabStock Entry`
+						WHERE
+							work_order=%s
+							AND purpose='Manufacture'
+							AND docstatus=1
+					)
+			""", (self.name, ))[0][0])
+		if process_loss_qty is not None:
+			self.db_set('process_loss_qty', process_loss_qty)
+
 	def update_production_plan_status(self):
 		production_plan = frappe.get_doc('Production Plan', self.production_plan)
 		produced_qty = 0
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 0b4592c..760cb7d 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -272,7 +272,7 @@
 		item_wise_qty = {}
 		if self.purpose == "Manufacture" and self.work_order:
 			for d in self.items:
-				if d.is_finished_item:
+				if d.is_finished_item or d.is_process_loss:
 					item_wise_qty.setdefault(d.item_code, []).append(d.qty)
 
 		for item_code, qty_list in iteritems(item_wise_qty):
@@ -333,7 +333,7 @@
 
 			if self.purpose == "Manufacture":
 				if validate_for_manufacture:
-					if d.is_finished_item or d.is_scrap_item:
+					if d.is_finished_item or d.is_scrap_item or d.is_process_loss:
 						d.s_warehouse = None
 						if not d.t_warehouse:
 							frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
@@ -465,7 +465,7 @@
 		"""
 		# Set rate for outgoing items
 		outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate)
-		finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item)
+		finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss)
 
 		# Set basic rate for incoming items
 		for d in self.get('items'):
@@ -486,6 +486,8 @@
 					raise_error_if_no_rate=raise_error_if_no_rate)
 
 			d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
+			if d.is_process_loss:
+				d.basic_rate = flt(0.)
 			d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
 
 	def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
@@ -1043,6 +1045,7 @@
 
 		self.set_scrap_items()
 		self.set_actual_qty()
+		self.update_items_for_process_loss()
 		self.validate_customer_provided_item()
 		self.calculate_rate_and_amount()
 
@@ -1400,6 +1403,7 @@
 				get_default_cost_center(item_dict[d], company = self.company))
 			se_child.is_finished_item = item_dict[d].get("is_finished_item", 0)
 			se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
+			se_child.is_process_loss = item_dict[d].get("is_process_loss", 0)
 
 			for field in ["idx", "po_detail", "original_item",
 				"expense_account", "description", "item_name", "serial_no", "batch_no"]:
@@ -1578,6 +1582,29 @@
 			if material_request and material_request not in material_requests:
 				material_requests.append(material_request)
 				frappe.db.set_value('Material Request', material_request, 'transfer_status', status)
+				
+	def update_items_for_process_loss(self):
+		process_loss_dict = {}
+		for d in self.get("items"):
+			if not d.is_process_loss:
+				continue
+
+			scrap_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_scrap_warehouse")
+			if scrap_warehouse is not None:
+				d.t_warehouse = scrap_warehouse
+			d.is_scrap_item = 0
+
+			if d.item_code not in process_loss_dict:
+				process_loss_dict[d.item_code] = [flt(0), flt(0)]
+			process_loss_dict[d.item_code][0] += flt(d.transfer_qty)
+			process_loss_dict[d.item_code][1] += flt(d.qty)
+
+		for d in self.get("items"):
+			if not d.is_finished_item or d.item_code not in process_loss_dict:
+				continue
+			# Assumption: 1 finished item has 1 row.
+			d.transfer_qty -= process_loss_dict[d.item_code][0]
+			d.qty -= process_loss_dict[d.item_code][1]
 
 	def set_serial_no_batch_for_finished_good(self):
 		args = {}
diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js
new file mode 100644
index 0000000..285ae4f
--- /dev/null
+++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js
@@ -0,0 +1,27 @@
+QUnit.module('Stock');
+
+QUnit.test("test manufacture from bom", function(assert) {
+	assert.expect(2);
+	let done = assert.async();
+	frappe.run_serially([
+		() => {
+			return frappe.tests.make("Stock Entry", [
+				{ purpose: "Manufacture" },
+				{ from_bom: 1 },
+				{ bom_no: "BOM-_Test Item - Non Whole UOM-001" },
+				{ fg_completed_qty: 2 }
+			]);
+		},
+		() => cur_frm.save(),
+		() => frappe.click_button("Update Rate and Availability"),
+		() => {
+			assert.ok(cur_frm.doc.items[1] === 0.75, " Finished Item Qty correct");
+			assert.ok(cur_frm.doc.items[2] === 0.25, " Process Loss Item Qty correct");
+		},
+		() => frappe.tests.click_button('Submit'),
+		() => frappe.tests.click_button('Yes'),
+		() => frappe.timeout(0.3),
+		() => done()
+	]);
+});
+
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 22f412a..df65706 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -19,6 +19,7 @@
   "is_finished_item",
   "is_scrap_item",
   "quality_inspection",
+  "is_process_loss",
   "subcontracted_item",
   "section_break_8",
   "description",
@@ -543,13 +544,19 @@
    "no_copy": 1,
    "print_hide": 1,
    "read_only": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "is_process_loss",
+   "fieldtype": "Check",
+   "label": "Is Process Loss"
   }
  ],
  "idx": 1,
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-06-21 16:03:18.834880",
+ "modified": "2021-06-22 16:47:11.268975",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Entry Detail",
diff --git a/erpnext/stock/report/process_loss_report/__init__.py b/erpnext/stock/report/process_loss_report/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/process_loss_report/__init__.py
diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.js b/erpnext/stock/report/process_loss_report/process_loss_report.js
new file mode 100644
index 0000000..078b9e1
--- /dev/null
+++ b/erpnext/stock/report/process_loss_report/process_loss_report.js
@@ -0,0 +1,37 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Process Loss Report"] = {
+	filters: [
+    {
+      label: __("Company"),
+      fieldname: "company",
+      fieldtype: "Link",
+      options: "Company",
+      mandatory: true,
+      default: frappe.defaults.get_user_default("Company"),
+    },
+		{
+      label: __("Item"),
+      fieldname: "item",
+      fieldtype: "Link",
+      options: "Item",
+      mandatory: false,
+		},
+    {
+      label: __("From Date"),
+      fieldname: "from_date",
+      fieldtype: "Date",
+      mandatory: true,
+      default: frappe.datetime.year_start(),
+    },
+    {
+      label: __("To Date"),
+      fieldname: "to_date",
+      fieldtype: "Date",
+      mandatory: true,
+      default: frappe.datetime.get_today(),
+    },
+	]
+};
diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.json b/erpnext/stock/report/process_loss_report/process_loss_report.json
new file mode 100644
index 0000000..afe4aff
--- /dev/null
+++ b/erpnext/stock/report/process_loss_report/process_loss_report.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-08-24 16:38:15.233395",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-08-24 16:38:15.233395",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Process Loss Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Work Order",
+ "report_name": "Process Loss Report",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Manufacturing User"
+  },
+  {
+   "role": "Stock User"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.py b/erpnext/stock/report/process_loss_report/process_loss_report.py
new file mode 100644
index 0000000..be0f015
--- /dev/null
+++ b/erpnext/stock/report/process_loss_report/process_loss_report.py
@@ -0,0 +1,132 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from typing import Dict, List, Tuple
+
+Filters = frappe._dict
+Row = frappe._dict
+Data = List[Row]
+Columns = List[Dict[str, str]]
+QueryArgs = Dict[str, str]
+
+def execute(filters: Filters) -> Tuple[Columns, Data]:
+	columns = get_columns()
+	data = get_data(filters)
+	return columns, data
+
+def get_data(filters: Filters) -> Data:
+	query_args = get_query_args(filters)
+	data = run_query(query_args)
+	update_data_with_total_pl_value(data)
+	return data
+
+def get_columns() -> Columns:
+	return [
+		{
+			'label': 'Work Order',
+			'fieldname': 'name',
+			'fieldtype': 'Link',
+			'options': 'Work Order',
+			'width': '200'
+		},
+		{
+			'label': 'Item',
+			'fieldname': 'production_item',
+			'fieldtype': 'Link',
+			'options': 'Item',
+			'width': '100'
+		},
+		{
+			'label': 'Status',
+			'fieldname': 'status',
+			'fieldtype': 'Data',
+			'width': '100'
+		},
+		{
+			'label': 'Qty To Manufacture',
+			'fieldname': 'qty',
+			'fieldtype': 'Float',
+			'width': '150'
+		},
+		{
+			'label': 'Manufactured Qty',
+			'fieldname': 'produced_qty',
+			'fieldtype': 'Float',
+			'width': '150'
+		},
+		{
+			'label': 'Process Loss Qty',
+			'fieldname': 'process_loss_qty',
+			'fieldtype': 'Float',
+			'width': '150'
+		},
+		{
+			'label': 'Actual Manufactured Qty',
+			'fieldname': 'actual_produced_qty',
+			'fieldtype': 'Float',
+			'width': '150'
+		},
+		{
+			'label': 'Total FG Value',
+			'fieldname': 'total_fg_value',
+			'fieldtype': 'Float',
+			'width': '150'
+		},
+		{
+			'label': 'Total Raw Material Value',
+			'fieldname': 'total_rm_value',
+			'fieldtype': 'Float',
+			'width': '150'
+		},
+		{
+			'label': 'Total Process Loss Value',
+			'fieldname': 'total_pl_value',
+			'fieldtype': 'Float',
+			'width': '150'
+		},
+	]
+
+def get_query_args(filters: Filters) -> QueryArgs:
+	query_args = {}
+	query_args.update(filters)
+	query_args.update(
+		get_filter_conditions(filters)
+	)
+	return query_args
+
+def run_query(query_args: QueryArgs) -> Data:
+	return frappe.db.sql("""
+		SELECT 
+			wo.name, wo.status, wo.production_item, wo.qty,
+			wo.produced_qty, wo.process_loss_qty,
+			(wo.produced_qty - wo.process_loss_qty) as actual_produced_qty,
+			sum(se.total_incoming_value) as total_fg_value,
+			sum(se.total_outgoing_value) as total_rm_value
+		FROM
+			`tabWork Order` wo INNER JOIN `tabStock Entry` se
+			ON wo.name=se.work_order
+		WHERE
+			process_loss_qty > 0
+			AND wo.company = %(company)s
+			AND se.docstatus = 1
+			AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s
+			%(item_filter)s
+		GROUP BY
+			se.work_order
+	""", query_args, as_dict=1)
+
+def update_data_with_total_pl_value(data: Data) -> None:
+	for row in data:
+		value_per_unit_fg = row['total_fg_value'] / row['actual_produced_qty']
+		row['total_pl_value'] = row['process_loss_qty'] * value_per_unit_fg
+
+def get_filter_conditions(filters: Filters) -> QueryArgs:
+	filter_conditions = dict(item_filter="")
+	if "item" in filters:
+		production_item = filters.get("item")
+		filter_conditions.update(
+			{"item_filter": f"wo.production_item='{production_item}'"}
+		)
+	return filter_conditions
+