feat: Value Based and Numeric Quality Inspection

- Acceptance Formula is optional
- Choose between Value based and Numeric QI
- If numeric, select single or multiple readings
- Added Min, Max and Mean Values for numeric inspection to avoid formula usage
- Deprecated code cleanup in js file
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..f450128 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,8 +8,14 @@
  "field_order": [
   "specification",
   "value",
+  "value_based",
+  "single_reading",
   "column_break_3",
-  "acceptance_formula"
+  "formula_based_criteria",
+  "acceptance_formula",
+  "min_value",
+  "max_value",
+  "mean_value"
  ],
  "fields": [
   {
@@ -24,10 +30,11 @@
    "width": "200px"
   },
   {
+   "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)",
    "fieldname": "value",
    "fieldtype": "Data",
    "in_list_view": 1,
-   "label": "Acceptance Criteria",
+   "label": "Acceptance Criteria Value",
    "oldfieldname": "value",
    "oldfieldtype": "Data"
   },
@@ -36,17 +43,56 @@
    "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.: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><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"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval:!doc.value_based",
+   "fieldname": "single_reading",
+   "fieldtype": "Check",
+   "label": "Single Reading"
+  },
+  {
+   "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)",
+   "fieldname": "mean_value",
+   "fieldtype": "Float",
+   "label": "Mean Value"
+  },
+  {
+   "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)",
+   "fieldname": "min_value",
+   "fieldtype": "Float",
+   "label": "Minimum Value"
+  },
+  {
+   "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)",
+   "fieldname": "max_value",
+   "fieldtype": "Float",
+   "label": "Maximum Value"
+  },
+  {
+   "default": "0",
+   "description": "Non-numeric Inspection.",
+   "fieldname": "value_based",
+   "fieldtype": "Check",
+   "label": "Value Based"
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2020-11-16 16:33:42.421842",
+ "modified": "2020-12-18 21:03:29.828723",
  "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 376848a..f0bf9ae 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() {
+			var 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
+				};
+			}
+		});
+	},
+
 	item_code: function(frm) {
 		if (frm.doc.item_code) {
 			return frm.call({
@@ -26,55 +74,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..a7a023b 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,84 @@
 				""".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:
+				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.value_based):
+			result = reading.get("reading_value") == reading.get("value")
+		else:
+			# numeric readings
+			if cint(reading.single_reading):
+				reading_1 = flt(reading.get("reading_1"))
+				result =  flt(reading.get("min_value")) <= reading_1 <= flt(reading.get("max_value"))
+			else:
+				result = self.min_max_criteria_passed(reading) and self.mean_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_field = reading.get("reading_" + str(i))
+			if reading_field is not None:
+				result = flt(reading.get("min_value")) <= flt(reading_field) <= flt(reading.get("max_value"))
+				if not result: return False
+		return True
+
+	def mean_criteria_passed(self, reading):
+		"""Determine whether mean of all readings is acceptable."""
+		if reading.get("mean_value"):
+			from statistics import mean
+			readings_list = []
+
 			for i in range(1, 11):
-				field = "reading_" + str(i)
-				data[field] = flt(reading.get(field)) or 0
+				reading_value = reading.get("reading_" + str(i))
+				if reading_value is not None:
+					readings_list.append(flt(reading_value))
 
-			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"))
+			actual_mean = mean(readings_list) if readings_list else 0
+			return True if actual_mean == reading.get("mean_value") else False
 
+		return True # no mean value, nothing to check
+
+	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.value_based):
+			data = {"reading_value": reading.get("reading_value")}
+		else:
+			# numeric readings
+			data = {"reading_1": flt(reading.get("reading_1"))}
+			if not cint(reading.single_reading):
+				# if multiple numeric readings add all readings to data
+				for i in range(2, 11):
+					field = "reading_" + str(i)
+					data[field] = flt(reading.get(field))
+		return data
 
 @frappe.whitelist()
 @frappe.validate_and_sanitize_search_inputs
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..db95fab 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,30 @@
  "engine": "InnoDB",
  "field_order": [
   "specification",
-  "value",
   "status",
+  "value",
+  "value_based",
   "column_break_4",
+  "formula_based_criteria",
   "acceptance_formula",
+  "min_value",
+  "max_value",
+  "mean_value",
   "section_break_3",
+  "reading_value",
+  "section_break_14",
+  "single_reading",
+  "section_break_12",
   "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 +47,11 @@
   },
   {
    "columns": 2,
+   "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)",
    "fieldname": "value",
    "fieldtype": "Data",
    "in_list_view": 1,
-   "label": "Acceptance Criteria",
+   "label": "Acceptance Criteria Value",
    "oldfieldname": "value",
    "oldfieldtype": "Data"
   },
@@ -56,6 +66,7 @@
   },
   {
    "columns": 1,
+   "depends_on": "eval:!doc.single_reading",
    "fieldname": "reading_2",
    "fieldtype": "Data",
    "in_list_view": 1,
@@ -65,6 +76,7 @@
   },
   {
    "columns": 1,
+   "depends_on": "eval:!doc.single_reading",
    "fieldname": "reading_3",
    "fieldtype": "Data",
    "in_list_view": 1,
@@ -73,6 +85,7 @@
    "oldfieldtype": "Data"
   },
   {
+   "depends_on": "eval:!doc.single_reading",
    "fieldname": "reading_4",
    "fieldtype": "Data",
    "label": "Reading 4",
@@ -80,6 +93,7 @@
    "oldfieldtype": "Data"
   },
   {
+   "depends_on": "eval:!doc.single_reading",
    "fieldname": "reading_5",
    "fieldtype": "Data",
    "label": "Reading 5",
@@ -87,6 +101,7 @@
    "oldfieldtype": "Data"
   },
   {
+   "depends_on": "eval:!doc.single_reading",
    "fieldname": "reading_6",
    "fieldtype": "Data",
    "label": "Reading 6",
@@ -94,6 +109,7 @@
    "oldfieldtype": "Data"
   },
   {
+   "depends_on": "eval:!doc.single_reading",
    "fieldname": "reading_7",
    "fieldtype": "Data",
    "label": "Reading 7",
@@ -101,6 +117,7 @@
    "oldfieldtype": "Data"
   },
   {
+   "depends_on": "eval:!doc.single_reading",
    "fieldname": "reading_8",
    "fieldtype": "Data",
    "label": "Reading 8",
@@ -108,6 +125,7 @@
    "oldfieldtype": "Data"
   },
   {
+   "depends_on": "eval:!doc.single_reading",
    "fieldname": "reading_9",
    "fieldtype": "Data",
    "label": "Reading 9",
@@ -115,6 +133,7 @@
    "oldfieldtype": "Data"
   },
   {
+   "depends_on": "eval:!doc.single_reading",
    "fieldname": "reading_10",
    "fieldtype": "Data",
    "label": "Reading 10",
@@ -133,15 +152,18 @@
    "options": "Accepted\nRejected"
   },
   {
+   "depends_on": "value_based",
    "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.: <b>reading_1 &gt; 0.2 and reading_1 &lt; 0.5</b><br>\nValue based eg.:  <b>reading_value in (\"A\", \"B\", \"C)</b>",
    "fieldname": "acceptance_formula",
    "fieldtype": "Code",
    "label": "Acceptance Criteria Formula"
@@ -153,12 +175,69 @@
   {
    "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.single_reading && !doc.value_based)",
+   "fieldname": "mean_value",
+   "fieldtype": "Float",
+   "label": "Mean Value"
+  },
+  {
+   "default": "0",
+   "fieldname": "single_reading",
+   "fieldtype": "Check",
+   "label": "Single Reading"
+  },
+  {
+   "depends_on": "eval:!doc.value_based",
+   "fieldname": "section_break_12",
+   "fieldtype": "Section Break",
+   "hide_border": 1
+  },
+  {
+   "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)",
+   "description": "Applied on each reading.",
+   "fieldname": "min_value",
+   "fieldtype": "Float",
+   "label": "Minimum Value"
+  },
+  {
+   "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)",
+   "description": "Applied on each reading.",
+   "fieldname": "max_value",
+   "fieldtype": "Float",
+   "label": "Maximum Value"
+  },
+  {
+   "default": "0",
+   "description": "Non-numeric Inspection.",
+   "fieldname": "value_based",
+   "fieldtype": "Check",
+   "label": "Value Based"
+  },
+  {
+   "depends_on": "value_based",
+   "fieldname": "reading_value",
+   "fieldtype": "Data",
+   "label": "Reading Value"
+  },
+  {
+   "depends_on": "eval:!doc.value_based",
+   "fieldname": "section_break_14",
+   "fieldtype": "Section Break",
+   "label": "Numeric Inspection"
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2020-11-16 16:34:29.947856",
+ "modified": "2020-12-18 21:02:04.865777",
  "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..7dd0feb 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,8 @@
 	if not template: return []
 
 	return frappe.get_all('Item Quality Inspection Parameter',
-		fields=["specification", "value", "acceptance_formula"],
+		fields=["specification", "value", "acceptance_formula",
+			"value_based", "formula_based_criteria", "single_reading",
+			"min_value", "max_value", "mean_value"],
 		filters={'parenttype': 'Quality Inspection Template', 'parent': template},
 		order_by="idx")
\ No newline at end of file