feat: provision to add scrap item in job card (#27483)

diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index f5bbac3..7dd38f4 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -38,6 +38,8 @@
   "total_time_in_mins",
   "section_break_8",
   "items",
+  "scrap_items_section",
+  "scrap_items",
   "corrective_operation_section",
   "for_job_card",
   "is_corrective_job_card",
@@ -392,11 +394,24 @@
    "fieldtype": "Link",
    "label": "Batch No",
    "options": "Batch"
+  },
+  {
+   "fieldname": "scrap_items_section",
+   "fieldtype": "Section Break",
+   "label": "Scrap Items"
+  },
+  {
+   "fieldname": "scrap_items",
+   "fieldtype": "Table",
+   "label": "Scrap Items",
+   "no_copy": 1,
+   "options": "Job Card Scrap Item",
+   "print_hide": 1
   }
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2021-09-13 21:34:15.177928",
+ "modified": "2021-09-14 00:38:46.873105",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Job Card",
diff --git a/erpnext/manufacturing/doctype/job_card_scrap_item/__init__.py b/erpnext/manufacturing/doctype/job_card_scrap_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_scrap_item/__init__.py
diff --git a/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json b/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json
new file mode 100644
index 0000000..9e9f1c4
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json
@@ -0,0 +1,82 @@
+{
+ "actions": [],
+ "creation": "2021-09-14 00:30:28.533884",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "item_code",
+  "item_name",
+  "column_break_3",
+  "description",
+  "quantity_and_rate",
+  "stock_qty",
+  "column_break_6",
+  "stock_uom"
+ ],
+ "fields": [
+  {
+   "fieldname": "item_code",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Scrap Item Code",
+   "options": "Item",
+   "reqd": 1
+  },
+  {
+   "fetch_from": "item_code.item_name",
+   "fieldname": "item_name",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Scrap Item Name"
+  },
+  {
+   "fieldname": "column_break_3",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fetch_from": "item_code.description",
+   "fieldname": "description",
+   "fieldtype": "Small Text",
+   "label": "Description",
+   "read_only": 1
+  },
+  {
+   "fieldname": "quantity_and_rate",
+   "fieldtype": "Section Break",
+   "label": "Quantity and Rate"
+  },
+  {
+   "fieldname": "stock_qty",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Qty",
+   "reqd": 1
+  },
+  {
+   "fieldname": "column_break_6",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fetch_from": "item_code.stock_uom",
+   "fieldname": "stock_uom",
+   "fieldtype": "Link",
+   "label": "Stock UOM",
+   "options": "UOM",
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-14 01:20:48.588052",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Job Card 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/job_card_scrap_item/job_card_scrap_item.py b/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.py
new file mode 100644
index 0000000..372df1b
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from frappe.model.document import Document
+
+
+class JobCardScrapItem(Document):
+	pass
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 6a942d5..707b3f6 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -404,6 +404,7 @@
 			'uom': item_doc.stock_uom,
 			'stock_uom': item_doc.stock_uom,
 			'rate': item_doc.valuation_rate or args.rate,
+			'source_warehouse': args.source_warehouse
 		})
 
 	if not args.do_not_save:
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index d87b5ec..85b5bfb 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -16,7 +16,7 @@
 	stop_unstop,
 )
 from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
-from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.doctype.item.test_item import create_item, make_item
 from erpnext.stock.doctype.stock_entry import test_stock_entry
 from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
 from erpnext.stock.utils import get_bin
@@ -768,6 +768,60 @@
 			total_pl_qty
 		)
 
+	def test_job_card_scrap_item(self):
+		items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test',
+			'Test RM Item 2 for Scrap Item Test']
+
+		company = '_Test Company with perpetual inventory'
+		for item_code in items:
+			create_item(item_code = item_code, is_stock_item = 1,
+				is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1')
+
+		item = 'Test FG Item for Scrap Item Test'
+		raw_materials = ['Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test']
+		if not frappe.db.get_value('BOM', {'item': item}):
+			bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True)
+			bom.with_operations = 1
+			bom.append('operations', {
+				'operation': '_Test Operation 1',
+				'workstation': '_Test Workstation 1',
+				'hour_rate': 20,
+				'time_in_mins': 60
+			})
+
+			bom.submit()
+
+		wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1)
+		job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
+		update_job_card(job_card)
+
+		stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
+		for row in stock_entry.items:
+			if row.is_scrap_item:
+				self.assertEqual(row.qty, 1)
+
+def update_job_card(job_card):
+	job_card_doc = frappe.get_doc('Job Card', job_card)
+	job_card_doc.set('scrap_items', [
+		{
+			'item_code': 'Test RM Item 1 for Scrap Item Test',
+			'stock_qty': 2
+		},
+		{
+			'item_code': 'Test RM Item 2 for Scrap Item Test',
+			'stock_qty': 2
+		},
+	])
+
+	job_card_doc.append('time_logs', {
+		'from_time': now(),
+		'time_in_mins': 60,
+		'completed_qty': job_card_doc.for_quantity
+	})
+
+	job_card_doc.submit()
+
+
 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/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 0459489..1c9b961 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -4,6 +4,7 @@
 from __future__ import unicode_literals
 
 import json
+from collections import defaultdict
 
 import frappe
 from frappe import _
@@ -684,7 +685,7 @@
 
 	def validate_bom(self):
 		for d in self.get('items'):
-			if d.bom_no and (d.t_warehouse != getattr(self, "pro_doc", frappe._dict()).scrap_warehouse):
+			if d.bom_no and d.is_finished_item:
 				item_code = d.original_item or d.item_code
 				validate_bom_no(item_code, d.bom_no)
 
@@ -1191,13 +1192,88 @@
 
 		# item dict = { item_code: {qty, description, stock_uom} }
 		item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty,
-			fetch_exploded = 0, fetch_scrap_items = 1)
+			fetch_exploded = 0, fetch_scrap_items = 1) or {}
 
 		for item in itervalues(item_dict):
 			item.from_warehouse = ""
 			item.is_scrap_item = 1
+
+		for row in self.get_scrap_items_from_job_card():
+			if row.stock_qty <= 0:
+				continue
+
+			item_row = item_dict.get(row.item_code)
+			if not item_row:
+				item_row = frappe._dict({})
+
+			item_row.update({
+				'uom': row.stock_uom,
+				'from_warehouse': '',
+				'qty': row.stock_qty + flt(item_row.stock_qty),
+				'converison_factor': 1,
+				'is_scrap_item': 1,
+				'item_name': row.item_name,
+				'description': row.description,
+				'allow_zero_valuation_rate': 1
+			})
+
+			item_dict[row.item_code] = item_row
+
 		return item_dict
 
+	def get_scrap_items_from_job_card(self):
+		if not self.pro_doc:
+			self.set_work_order_details()
+
+		scrap_items = frappe.db.sql('''
+			SELECT
+				JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description
+			FROM
+				`tabJob Card` JC, `tabJob Card Scrap Item` JCSI
+			WHERE
+				JCSI.parent = JC.name AND JC.docstatus = 1
+				AND JCSI.item_code IS NOT NULL AND JC.work_order = %s
+			GROUP BY
+				JCSI.item_code
+		''', self.work_order, as_dict=1)
+
+		pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty)
+		if pending_qty <=0:
+			return []
+
+		used_scrap_items = self.get_used_scrap_items()
+		for row in scrap_items:
+			row.stock_qty -= flt(used_scrap_items.get(row.item_code))
+			row.stock_qty = (row.stock_qty) * flt(self.fg_completed_qty) / flt(pending_qty)
+
+			if used_scrap_items.get(row.item_code):
+				used_scrap_items[row.item_code] -= row.stock_qty
+
+			if cint(frappe.get_cached_value('UOM', row.stock_uom, 'must_be_whole_number')):
+				row.stock_qty = frappe.utils.ceil(row.stock_qty)
+
+		return scrap_items
+
+	def get_used_scrap_items(self):
+		used_scrap_items = defaultdict(float)
+		data = frappe.get_all(
+			'Stock Entry',
+			fields = [
+				'`tabStock Entry Detail`.`item_code`', '`tabStock Entry Detail`.`qty`'
+			],
+			filters = [
+				['Stock Entry', 'work_order', '=', self.work_order],
+				['Stock Entry Detail', 'is_scrap_item', '=', 1],
+				['Stock Entry', 'docstatus', '=', 1],
+				['Stock Entry', 'purpose', 'in', ['Repack', 'Manufacture']]
+			]
+		)
+
+		for row in data:
+			used_scrap_items[row.item_code] += row.qty
+
+		return used_scrap_items
+
 	def get_unconsumed_raw_materials(self):
 		wo = frappe.get_doc("Work Order", self.work_order)
 		wo_items = frappe.get_all('Work Order Item',
@@ -1417,8 +1493,8 @@
 			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"]:
+			for field in ["idx", "po_detail", "original_item", "expense_account",
+				"description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]:
 				if item_dict[d].get(field):
 					se_child.set(field, item_dict[d].get(field))