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
This commit is contained in:
marination 2020-12-18 21:37:19 +05:30
parent d4ee05c5fb
commit d6277cdc7f
6 changed files with 268 additions and 87 deletions

View File

@ -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",

View File

@ -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 @@ frappe.ui.form.on("Quality Inspection", {
}
});
}
}
})
// 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');
},
})

View File

@ -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",

View File

@ -6,7 +6,7 @@ import frappe
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 @@ class QualityInspection(Document):
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 @@ class QualityInspection(Document):
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 @@ class QualityInspection(Document):
""".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)
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 = []
condition = reading.acceptance_formula
data = {}
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

View File

@ -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",

View File

@ -13,6 +13,8 @@ def get_template_details(template):
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")