Merge branch 'develop' into qi-ux
diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
index 888bc2d..9b980a1 100644
--- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
+++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
@@ -8,7 +8,11 @@
  "field_order": [
   "specification",
   "value",
+  "non_numeric",
   "column_break_3",
+  "min_value",
+  "max_value",
+  "formula_based_criteria",
   "acceptance_formula"
  ],
  "fields": [
@@ -21,13 +25,14 @@
    "oldfieldtype": "Data",
    "print_width": "200px",
    "reqd": 1,
-   "width": "200px"
+   "width": "100px"
   },
   {
+   "depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
    "fieldname": "value",
    "fieldtype": "Data",
    "in_list_view": 1,
-   "label": "Acceptance Criteria",
+   "label": "Acceptance Criteria Value",
    "oldfieldname": "value",
    "oldfieldtype": "Data"
   },
@@ -36,17 +41,45 @@
    "fieldtype": "Column Break"
   },
   {
-   "description": "Simple Python formula based on numeric Readings.<br> Example 1: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 &lt; 10</b>",
+   "depends_on": "formula_based_criteria",
+   "description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nNumeric eg. 2: <b>mean &gt; 3.5</b> (mean of populated fields)<br>\nValue based eg.:  <b>reading_value in (\"A\", \"B\", \"C)</b>",
    "fieldname": "acceptance_formula",
    "fieldtype": "Code",
-   "in_list_view": 1,
    "label": "Acceptance Criteria Formula"
+  },
+  {
+   "default": "0",
+   "fieldname": "formula_based_criteria",
+   "fieldtype": "Check",
+   "label": "Formula Based Criteria"
+  },
+  {
+   "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
+   "fieldname": "min_value",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Minimum Value"
+  },
+  {
+   "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
+   "fieldname": "max_value",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Maximum Value"
+  },
+  {
+   "default": "0",
+   "fieldname": "non_numeric",
+   "fieldtype": "Check",
+   "in_list_view": 1,
+   "label": "Non-Numeric",
+   "width": "80px"
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2020-11-16 16:33:42.421842",
+ "modified": "2020-12-21 11:37:55.387677",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Item Quality Inspection Parameter",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
index 03e3de1..544bc2c 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
@@ -4,6 +4,54 @@
 cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
 
 frappe.ui.form.on("Quality Inspection", {
+
+	setup: function(frm) {
+		frm.set_query("batch_no", function() {
+			return {
+				filters: {
+					"item": frm.doc.item_code
+				}
+			};
+		});
+
+		// Serial No based on item_code
+		frm.set_query("item_serial_no", function() {
+			let filters = {};
+			if (frm.doc.item_code) {
+				filters = {
+					'item_code': frm.doc.item_code
+				};
+			}
+			return { filters: filters };
+		});
+
+		// item code based on GRN/DN
+		frm.set_query("item_code", function(doc) {
+			let doctype = doc.reference_type;
+
+			if (doc.reference_type !== "Job Card") {
+				doctype = (doc.reference_type == "Stock Entry") ?
+					"Stock Entry Detail" : doc.reference_type + " Item";
+			}
+
+			if (doc.reference_type && doc.reference_name) {
+				let filters = {
+					"from": doctype,
+					"inspection_type": doc.inspection_type
+				};
+
+				if (doc.reference_type == doctype)
+					filters["reference_name"] = doc.reference_name;
+				else
+					filters["parent"] = doc.reference_name;
+
+				return {
+					query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
+					filters: filters
+				};
+			}
+		});
+
 	refresh: function(frm) {
 		// Ignore cancellation of reference doctype on cancel all.
 		frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type];
@@ -31,55 +79,5 @@
 				}
 			});
 		}
-	}
-})
-
-// item code based on GRN/DN
-cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) {
-	let doctype = doc.reference_type;
-
-	if (doc.reference_type !== "Job Card") {
-		doctype = (doc.reference_type == "Stock Entry") ?
-			"Stock Entry Detail" : doc.reference_type + " Item";
-	}
-
-	if (doc.reference_type && doc.reference_name) {
-		let filters = {
-			"from": doctype,
-			"inspection_type": doc.inspection_type
-		};
-
-		if (doc.reference_type == doctype)
-			filters["reference_name"] = doc.reference_name;
-		else
-			filters["parent"] = doc.reference_name;
-
-		return {
-			query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
-			filters: filters
-		};
-	}
-},
-
-// Serial No based on item_code
-cur_frm.fields_dict['item_serial_no'].get_query = function(doc, cdt, cdn) {
-	var filters = {};
-	if (doc.item_code) {
-		filters = {
-			'item_code': doc.item_code
-		}
-	}
-	return { filters: filters }
-}
-
-cur_frm.set_query("batch_no", function(doc) {
-	return {
-		filters: {
-			"item": doc.item_code
-		}
-	}
-})
-
-cur_frm.add_fetch('item_code', 'item_name', 'item_name');
-cur_frm.add_fetch('item_code', 'description', 'description');
-
+	},
+});
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
index f6d7619..edfe7e9 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
@@ -136,6 +136,7 @@
    "width": "50%"
   },
   {
+   "fetch_from": "item_code.item_name",
    "fieldname": "item_name",
    "fieldtype": "Data",
    "in_global_search": 1,
@@ -143,6 +144,7 @@
    "read_only": 1
   },
   {
+   "fetch_from": "item_code.description",
    "fieldname": "description",
    "fieldtype": "Small Text",
    "label": "Description",
@@ -236,7 +238,7 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2020-11-19 17:06:05.409963",
+ "modified": "2020-12-18 19:59:55.710300",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Quality Inspection",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index ae4eb9b..f582658 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -6,7 +6,7 @@
 from frappe.model.document import Document
 from frappe.model.mapper import get_mapped_doc
 from frappe import _
-from frappe.utils import flt
+from frappe.utils import flt, cint
 from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \
 	import get_template_details
 
@@ -16,7 +16,7 @@
 			self.get_item_specification_details()
 
 		if self.readings:
-			self.set_status_based_on_acceptance_formula()
+			self.inspect_and_set_status()
 
 	def get_item_specification_details(self):
 		if not self.quality_inspection_template:
@@ -29,9 +29,7 @@
 		parameters = get_template_details(self.quality_inspection_template)
 		for d in parameters:
 			child = self.append('readings', {})
-			child.specification = d.specification
-			child.value = d.value
-			child.acceptance_formula = d.acceptance_formula
+			child.update(d)
 			child.status = "Accepted"
 
 	def get_quality_inspection_template(self):
@@ -76,28 +74,77 @@
 				""".format(parent_doc=self.reference_type, child_doc=doctype),
 					(quality_inspection, self.modified, self.reference_name, self.item_code))
 
-	def set_status_based_on_acceptance_formula(self):
+	def inspect_and_set_status(self):
 		for reading in self.readings:
-			if not reading.acceptance_formula: continue
+			if reading.formula_based_criteria:
+				self.set_status_based_on_acceptance_formula(reading)
+			else:
+				# if not formula based check acceptance values set
+				self.set_status_based_on_acceptance_values(reading)
 
-			condition = reading.acceptance_formula
-			data = {}
+	def set_status_based_on_acceptance_values(self, reading):
+		if cint(reading.non_numeric):
+			result = reading.get("reading_value") == reading.get("value")
+		else:
+			# numeric readings
+			result = self.min_max_criteria_passed(reading)
+
+		reading.status = "Accepted" if result else "Rejected"
+
+	def min_max_criteria_passed(self, reading):
+		"""Determine whether all readings fall in the acceptable range."""
+		for i in range(1, 11):
+			reading_value = reading.get("reading_" + str(i))
+			if reading_value is not None and reading_value.strip():
+				result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value"))
+				if not result: return False
+		return True
+
+	def set_status_based_on_acceptance_formula(self, reading):
+		if not reading.acceptance_formula:
+			frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx),
+				title=_("Missing Formula"))
+
+		condition = reading.acceptance_formula
+		data = self.get_formula_evaluation_data(reading)
+
+		try:
+			result = frappe.safe_eval(condition, None, data)
+			reading.status = "Accepted" if result else "Rejected"
+		except NameError as e:
+			field = frappe.bold(e.args[0].split()[1])
+			frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
+				.format(reading.idx, field),
+				title=_("Invalid Formula"))
+		except Exception:
+			frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
+				title=_("Invalid Formula"))
+
+	def get_formula_evaluation_data(self, reading):
+		data = {}
+		if cint(reading.non_numeric):
+			data = {"reading_value": reading.get("reading_value")}
+		else:
+			# numeric readings
 			for i in range(1, 11):
 				field = "reading_" + str(i)
-				data[field] = flt(reading.get(field)) or 0
+				data[field] = flt(reading.get(field))
+			data["mean"] = self.calculate_mean(reading)
 
-			try:
-				result = frappe.safe_eval(condition, None, data)
-				reading.status = "Accepted" if result else "Rejected"
-			except SyntaxError:
-				frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
-					title=_("Invalid Formula"))
-			except NameError as e:
-				field = frappe.bold(e.args[0].split()[1])
-				frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
-					.format(reading.idx, field),
-					title=_("Invalid Formula"))
+		return data
 
+	def calculate_mean(self, reading):
+		"""Calculate mean of all non-empty readings."""
+		from statistics import mean
+		readings_list = []
+
+		for i in range(1, 11):
+			reading_value = reading.get("reading_" + str(i))
+			if reading_value is not None and reading_value.strip():
+				readings_list.append(flt(reading_value))
+
+		actual_mean = mean(readings_list) if readings_list else 0
+		return actual_mean
 
 @frappe.whitelist()
 @frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index 2c40009..d0bfb46 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -44,24 +44,61 @@
 		qa.delete()
 		dn.delete()
 
+	def test_value_based_qi_readings(self):
+		# Test QI based on acceptance values (Non formula)
+		dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
+		readings = [{
+			"specification": "Iron Content", # numeric reading
+			"min_value": 0.1,
+			"max_value": 0.9,
+			"reading_1": "0.4"
+		},
+		{
+			"specification": "Particle Inspection Needed", # non-numeric reading
+			"non_numeric": 1,
+			"value": "Yes",
+			"reading_value": "Yes"
+		}]
+
+		qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
+			readings=readings, do_not_save=True)
+		qa.save()
+
+		# status must be auto set as per formula
+		self.assertEqual(qa.readings[0].status, "Accepted")
+		self.assertEqual(qa.readings[1].status, "Accepted")
+
+		qa.delete()
+		dn.delete()
+
 	def test_formula_based_qi_readings(self):
 		dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
 		readings = [{
-			"specification": "Iron Content",
+			"specification": "Iron Content", # numeric reading
+			"formula_based_criteria": 1,
 			"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50",
-			"reading_1": 0.4
+			"reading_1": "0.4"
 		},
 		{
-			"specification": "Calcium Content",
+			"specification": "Calcium Content", # numeric reading
+			"formula_based_criteria": 1,
 			"acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
-			"reading_1": 0.7
+			"reading_1": "0.7"
 		},
 		{
-			"specification": "Mg Content",
-			"acceptance_formula": "(reading_1 + reading_2 + reading_3) / 3 < 0.9",
-			"reading_1": 0.5,
-			"reading_2": 0.7,
+			"specification": "Mg Content", # numeric reading
+			"formula_based_criteria": 1,
+			"acceptance_formula": "mean < 0.9",
+			"reading_1": "0.5",
+			"reading_2": "0.7",
 			"reading_3": "random text" # check if random string input causes issues
+		},
+		{
+			"specification": "Calcium Content", # non-numeric reading
+			"formula_based_criteria": 1,
+			"non_numeric": 1,
+			"acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')",
+			"reading_value": "Grade B"
 		}]
 
 		qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
@@ -72,6 +109,7 @@
 		self.assertEqual(qa.readings[0].status, "Accepted")
 		self.assertEqual(qa.readings[1].status, "Rejected")
 		self.assertEqual(qa.readings[2].status, "Accepted")
+		self.assertEqual(qa.readings[3].status, "Accepted")
 
 		qa.delete()
 		dn.delete()
@@ -86,8 +124,11 @@
 	qa.item_code = args.item_code or "_Test Item with QA"
 	qa.sample_size = 1
 	qa.inspected_by = frappe.session.user
+	qa.status = args.status or "Accepted"
 
-	readings = args.readings or {"specification": "Size", "status": args.status}
+	readings = args.readings or {"specification": "Size", "min_value": 0, "max_value": 10}
+	if args.status == "Rejected":
+		readings["reading_1"] = "12" # status is auto set in child on save
 
 	if isinstance(readings, list):
 		for entry in readings:
diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
index c1976dd..0792f26 100644
--- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
+++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
@@ -7,21 +7,27 @@
  "engine": "InnoDB",
  "field_order": [
   "specification",
-  "value",
   "status",
+  "value",
+  "non_numeric",
   "column_break_4",
+  "min_value",
+  "max_value",
+  "formula_based_criteria",
   "acceptance_formula",
   "section_break_3",
+  "reading_value",
+  "section_break_14",
   "reading_1",
   "reading_2",
   "reading_3",
-  "column_break_10",
   "reading_4",
+  "column_break_10",
   "reading_5",
   "reading_6",
-  "column_break_14",
   "reading_7",
   "reading_8",
+  "column_break_14",
   "reading_9",
   "reading_10"
  ],
@@ -38,10 +44,11 @@
   },
   {
    "columns": 2,
+   "depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
    "fieldname": "value",
    "fieldtype": "Data",
    "in_list_view": 1,
-   "label": "Acceptance Criteria",
+   "label": "Acceptance Criteria Value",
    "oldfieldname": "value",
    "oldfieldtype": "Data"
   },
@@ -133,15 +140,18 @@
    "options": "Accepted\nRejected"
   },
   {
+   "depends_on": "non_numeric",
    "fieldname": "section_break_3",
-   "fieldtype": "Section Break"
+   "fieldtype": "Section Break",
+   "label": "Value Based Inspection"
   },
   {
    "fieldname": "column_break_4",
    "fieldtype": "Column Break"
   },
   {
-   "description": "Simple Python formula based on numeric Readings.<br> Example 1: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 &lt; 10</b>",
+   "depends_on": "formula_based_criteria",
+   "description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nNumeric eg. 2: <b>mean &gt; 3.5</b> (mean of populated fields)<br>\nValue based eg.:  <b>reading_value in (\"A\", \"B\", \"C)</b>",
    "fieldname": "acceptance_formula",
    "fieldtype": "Code",
    "label": "Acceptance Criteria Formula"
@@ -153,12 +163,50 @@
   {
    "fieldname": "column_break_14",
    "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "formula_based_criteria",
+   "fieldtype": "Check",
+   "label": "Formula Based Criteria"
+  },
+  {
+   "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
+   "description": "Applied on each reading.",
+   "fieldname": "min_value",
+   "fieldtype": "Float",
+   "label": "Minimum Value"
+  },
+  {
+   "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)",
+   "description": "Applied on each reading.",
+   "fieldname": "max_value",
+   "fieldtype": "Float",
+   "label": "Maximum Value"
+  },
+  {
+   "depends_on": "non_numeric",
+   "fieldname": "reading_value",
+   "fieldtype": "Data",
+   "label": "Reading Value"
+  },
+  {
+   "depends_on": "eval:!doc.non_numeric",
+   "fieldname": "section_break_14",
+   "fieldtype": "Section Break",
+   "label": "Numeric Inspection"
+  },
+  {
+   "default": "0",
+   "fieldname": "non_numeric",
+   "fieldtype": "Check",
+   "label": "Non-Numeric"
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2020-11-16 16:34:29.947856",
+ "modified": "2020-12-21 11:36:24.885019",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Quality Inspection Reading",
diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
index e284846..c5a7974 100644
--- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
+++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
@@ -13,6 +13,7 @@
 	if not template: return []
 
 	return frappe.get_all('Item Quality Inspection Parameter',
-		fields=["specification", "value", "acceptance_formula"],
+		fields=["specification", "value", "acceptance_formula",
+			"non_numeric", "formula_based_criteria", "min_value", "max_value"],
 		filters={'parenttype': 'Quality Inspection Template', 'parent': template},
 		order_by="idx")
\ No newline at end of file