Merge branch 'develop' into job-card-excess-transfer
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 91eb4a0..35be388 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -26,15 +26,23 @@
 	refresh: function(frm) {
 		frappe.flags.pause_job = 0;
 		frappe.flags.resume_job = 0;
+		let has_items = frm.doc.items && frm.doc.items.length;
 
-		if(!frm.doc.__islocal && frm.doc.items && frm.doc.items.length) {
-			if (frm.doc.for_quantity != frm.doc.transferred_qty) {
+		if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) {
+			let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
+			let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
+
+			if (to_request || excess_transfer_allowed) {
 				frm.add_custom_button(__("Material Request"), () => {
 					frm.trigger("make_material_request");
 				});
 			}
 
-			if (frm.doc.for_quantity != frm.doc.transferred_qty) {
+			// check if any row has untransferred materials
+			// in case of multiple items in JC
+			let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
+
+			if (to_transfer || excess_transfer_allowed) {
 				frm.add_custom_button(__("Material Transfer"), () => {
 					frm.trigger("make_stock_entry");
 				}).addClass("btn-primary");
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index 046e2fd..f5bbac3 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -185,7 +185,7 @@
    "default": "0",
    "fieldname": "transferred_qty",
    "fieldtype": "Float",
-   "label": "Transferred Qty",
+   "label": "FG Qty from Transferred Raw Materials",
    "read_only": 1
   },
   {
@@ -396,10 +396,11 @@
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2021-03-16 15:59:32.766484",
+ "modified": "2021-09-13 21:34:15.177928",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Job Card",
+ "naming_rule": "By \"Naming Series\" field",
  "owner": "Administrator",
  "permissions": [
   {
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index ceae63c..3209546 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -1,9 +1,6 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
-
-from __future__ import unicode_literals
-
 import datetime
 import json
 
@@ -37,6 +34,10 @@
 class JobCardCancelError(frappe.ValidationError): pass
 
 class JobCard(Document):
+	def onload(self):
+		excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
+		self.set_onload("job_card_excess_transfer", excess_transfer)
+
 	def validate(self):
 		self.validate_time_logs()
 		self.set_status()
@@ -449,6 +450,7 @@
 			frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty))
 
 	def set_transferred_qty(self, update_status=False):
+		"Set total FG Qty for which RM was transferred."
 		if not self.items:
 			self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
 
@@ -457,6 +459,7 @@
 			return
 
 		if self.items:
+			# sum of 'For Quantity' of Stock Entries against JC
 			self.transferred_qty = frappe.db.get_value('Stock Entry', {
 				'job_card': self.name,
 				'work_order': self.work_order,
@@ -500,7 +503,9 @@
 			self.status = 'Work In Progress'
 
 		if (self.docstatus == 1 and
-			(self.for_quantity == self.transferred_qty or not self.items)):
+			(self.for_quantity <= self.transferred_qty or not self.items)):
+			# consider excess transfer
+			# completed qty is checked via separate validation
 			self.status = 'Completed'
 
 		if self.status != 'Completed':
@@ -618,7 +623,11 @@
 	def set_missing_values(source, target):
 		target.purpose = "Material Transfer for Manufacture"
 		target.from_bom = 1
-		target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
+
+		# avoid negative 'For Quantity'
+		pending_fg_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
+		target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
+
 		target.set_transfer_qty()
 		target.calculate_rate_and_amount()
 		target.set_missing_values()
diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py
index 80295bb..57336e1 100644
--- a/erpnext/manufacturing/doctype/job_card/test_job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py
@@ -1,22 +1,38 @@
 # -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
-from __future__ import unicode_literals
-
 import unittest
 
 import frappe
 from frappe.utils import random_string
 
-from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
+from erpnext.manufacturing.doctype.job_card.job_card import (
+	make_stock_entry as make_stock_entry_from_jc,
+	OperationMismatchError,
+	OverlapError
+)
 from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
 from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
 
 
 class TestJobCard(unittest.TestCase):
 
 	def setUp(self):
-		self.work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
+		transfer_material_against, source_warehouse = None, None
+		tests_that_transfer_against_jc = ("test_job_card_multiple_materials_transfer",
+			"test_job_card_excess_material_transfer")
+
+		if self._testMethodName in tests_that_transfer_against_jc:
+			transfer_material_against = "Job Card"
+			source_warehouse = "Stores - _TC"
+
+		self.work_order = make_wo_order_test_record(
+			item="_Test FG Item 2",
+			qty=2,
+			transfer_material_against=transfer_material_against,
+			source_warehouse=source_warehouse
+		)
 
 	def tearDown(self):
 		frappe.db.rollback()
@@ -96,3 +112,84 @@
 			"employee": employee,
 		})
 		self.assertRaises(OverlapError, jc2.save)
+
+	def test_job_card_multiple_materials_transfer(self):
+		"Test transferring RMs separately against Job Card with multiple RMs."
+		make_stock_entry(
+			item_code="_Test Item",
+			target="Stores - _TC",
+			qty=10,
+			basic_rate=100
+		)
+		make_stock_entry(
+			item_code="_Test Item Home Desktop Manufactured",
+			target="Stores - _TC",
+			qty=6,
+			basic_rate=100
+		)
+
+		job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
+		job_card = frappe.get_doc("Job Card", job_card_name)
+
+		transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
+		del transfer_entry_1.items[1] # transfer only 1 of 2 RMs
+		transfer_entry_1.insert()
+		transfer_entry_1.submit()
+
+		job_card.reload()
+
+		self.assertEqual(transfer_entry_1.fg_completed_qty, 2)
+		self.assertEqual(job_card.transferred_qty, 2)
+
+		# transfer second RM
+		transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
+		del transfer_entry_2.items[0]
+		transfer_entry_2.insert()
+		transfer_entry_2.submit()
+
+		# 'For Quantity' here will be 0 since
+		# transfer was made for 2 fg qty in first transfer Stock Entry
+		self.assertEqual(transfer_entry_2.fg_completed_qty, 0)
+
+	def test_job_card_excess_material_transfer(self):
+		"Test transferring more than required RM against Job Card."
+		make_stock_entry(item_code="_Test Item", target="Stores - _TC",
+			qty=25, basic_rate=100)
+		make_stock_entry(item_code="_Test Item Home Desktop Manufactured",
+			target="Stores - _TC", qty=15, basic_rate=100)
+
+		job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
+		job_card = frappe.get_doc("Job Card", job_card_name)
+
+		# fully transfer both RMs
+		transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
+		transfer_entry_1.insert()
+		transfer_entry_1.submit()
+
+		# transfer extra qty of both RM due to previously damaged RM
+		transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
+		# deliberately change 'For Quantity'
+		transfer_entry_2.fg_completed_qty = 1
+		transfer_entry_2.items[0].qty = 5
+		transfer_entry_2.items[1].qty = 3
+		transfer_entry_2.insert()
+		transfer_entry_2.submit()
+
+		job_card.reload()
+		self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
+
+		# Check if 'For Quantity' is negative
+		# as 'transferred_qty' > Qty to Manufacture
+		transfer_entry_3 = make_stock_entry_from_jc(job_card_name)
+		self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
+
+		job_card.append("time_logs", {
+			"from_time": "2021-01-01 00:01:00",
+			"to_time": "2021-01-01 06:00:00",
+			"completed_qty": 2
+		})
+		job_card.save()
+		job_card.submit()
+
+		# JC is Completed with excess transfer
+		self.assertEqual(job_card.status, "Completed")
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
index 024f784..01647d5 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
@@ -25,9 +25,12 @@
   "overproduction_percentage_for_sales_order",
   "column_break_16",
   "overproduction_percentage_for_work_order",
+  "job_card_section",
+  "add_corrective_operation_cost_in_finished_good_valuation",
+  "column_break_24",
+  "job_card_excess_transfer",
   "other_settings_section",
   "update_bom_costs_automatically",
-  "add_corrective_operation_cost_in_finished_good_valuation",
   "column_break_23",
   "make_serial_no_batch_from_work_order"
  ],
@@ -96,10 +99,10 @@
   },
   {
    "default": "0",
-   "description": "Allow multiple material consumptions against a Work Order",
+   "description": "Allow material consumptions without immediately manufacturing finished goods against a Work Order",
    "fieldname": "material_consumption",
    "fieldtype": "Check",
-   "label": "Allow Multiple Material Consumption"
+   "label": "Allow Continuous Material Consumption"
   },
   {
    "default": "0",
@@ -175,13 +178,29 @@
    "fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
    "fieldtype": "Check",
    "label": "Add Corrective Operation Cost in Finished Good Valuation"
+  },
+  {
+   "fieldname": "job_card_section",
+   "fieldtype": "Section Break",
+   "label": "Job Card"
+  },
+  {
+   "fieldname": "column_break_24",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "description": "Allow transferring raw materials even after the Required Quantity is fulfilled",
+   "fieldname": "job_card_excess_transfer",
+   "fieldtype": "Check",
+   "label": "Allow Excess Material Transfer"
   }
  ],
  "icon": "icon-wrench",
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2021-03-16 15:54:38.967341",
+ "modified": "2021-09-13 22:09:09.401559",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Manufacturing Settings",
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index bb43149..d87b5ec 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -1,9 +1,5 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
-
-
-from __future__ import unicode_literals
-
 import unittest
 
 import frappe
@@ -814,6 +810,7 @@
 	wo_order.get_items_and_operations_from_bom()
 	wo_order.sales_order = args.sales_order or None
 	wo_order.planned_start_date = args.planned_start_date or now()
+	wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
 
 	if args.source_warehouse:
 		for item in wo_order.get("required_items"):
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 4ccfa62..0459489 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -1264,9 +1264,9 @@
 		po_qty = frappe.db.sql("""select qty, produced_qty, material_transferred_for_manufacturing from
 			`tabWork Order` where name=%s""", self.work_order, as_dict=1)[0]
 
-		manufacturing_qty = flt(po_qty.qty)
+		manufacturing_qty = flt(po_qty.qty) or 1
 		produced_qty = flt(po_qty.produced_qty)
-		trans_qty = flt(po_qty.material_transferred_for_manufacturing)
+		trans_qty = flt(po_qty.material_transferred_for_manufacturing) or 1
 
 		for item in transferred_materials:
 			qty= item.qty