Merge branch 'develop' into batch-selector
diff --git a/erpnext/controllers/tests/test_item_variant.py b/erpnext/controllers/tests/test_item_variant.py
index c257215..813f0a0 100644
--- a/erpnext/controllers/tests/test_item_variant.py
+++ b/erpnext/controllers/tests/test_item_variant.py
@@ -6,6 +6,7 @@
from erpnext.stock.doctype.item.test_item import set_item_variant_settings
from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code
+from erpnext.stock.doctype.quality_inspection.test_quality_inspection import create_quality_inspection_parameter
from six import string_types
@@ -56,6 +57,8 @@
qc = frappe.new_doc("Quality Inspection Template")
qc.quality_inspection_template_name = qc_template
+
+ create_quality_inspection_parameter("Moisture")
qc.append('item_quality_inspection_parameter', {
"specification": "Moisture",
"value": "< 5%",
diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
index 3df7ba1..b681ed1 100644
--- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
@@ -23,8 +23,10 @@
self.assertEquals(appointment.status, 'Open')
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2))
self.assertEquals(appointment.status, 'Scheduled')
- create_encounter(appointment)
+ encounter = create_encounter(appointment)
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
+ encounter.cancel()
+ self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
def test_start_encounter(self):
patient, medical_department, practitioner = create_healthcare_docs()
diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
index a061c66..7fb159d 100644
--- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
@@ -5,10 +5,10 @@
import frappe
import unittest
-from frappe.utils import getdate, flt
+from frappe.utils import getdate, flt, nowdate
from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type
from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice
-from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient
+from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient, create_appointment
class TestTherapyPlan(unittest.TestCase):
def test_creation_on_encounter_submission(self):
@@ -28,6 +28,15 @@
frappe.get_doc(session).submit()
self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
+ patient, medical_department, practitioner = create_healthcare_docs()
+ appointment = create_appointment(patient, practitioner, nowdate())
+ session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
+ session = frappe.get_doc(session)
+ session.submit()
+ self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
+ session.cancel()
+ self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
+
def test_therapy_plan_from_template(self):
patient = create_patient()
template = create_therapy_plan_template()
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
index bc0ff1a..ac01c60 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
@@ -47,7 +47,7 @@
@frappe.whitelist()
-def make_therapy_session(therapy_plan, patient, therapy_type, company):
+def make_therapy_session(therapy_plan, patient, therapy_type, company, appointment=None):
therapy_type = frappe.get_doc('Therapy Type', therapy_type)
therapy_session = frappe.new_doc('Therapy Session')
@@ -58,6 +58,7 @@
therapy_session.duration = therapy_type.default_duration
therapy_session.rate = therapy_type.rate
therapy_session.exercises = therapy_type.exercises
+ therapy_session.appointment = appointment
if frappe.flags.in_test:
therapy_session.start_date = today()
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.js b/erpnext/healthcare/doctype/therapy_session/therapy_session.js
index a2b01c9..fd20003 100644
--- a/erpnext/healthcare/doctype/therapy_session/therapy_session.js
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.js
@@ -19,6 +19,15 @@
}
};
});
+
+ frm.set_query('appointment', function() {
+
+ return {
+ filters: {
+ 'status': ['in', ['Open', 'Scheduled']]
+ }
+ };
+ });
},
refresh: function(frm) {
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py
index 85d0970..c000544 100644
--- a/erpnext/healthcare/doctype/therapy_session/therapy_session.py
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py
@@ -43,7 +43,14 @@
self.update_sessions_count_in_therapy_plan()
insert_session_medical_record(self)
+ def on_update(self):
+ if self.appointment:
+ frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed')
+
def on_cancel(self):
+ if self.appointment:
+ frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open')
+
self.update_sessions_count_in_therapy_plan(on_cancel=True)
def update_sessions_count_in_therapy_plan(self, on_cancel=False):
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 923ed2f..043fafd 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -742,4 +742,5 @@
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
erpnext.patches.v13_0.add_po_to_global_search
erpnext.patches.v13_0.update_returned_qty_in_pr_dn
-erpnext.patches.v13_0.set_company_in_leave_ledger_entry
\ No newline at end of file
+erpnext.patches.v13_0.set_company_in_leave_ledger_entry
+erpnext.patches.v13_0.convert_qi_parameter_to_link_field
diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
new file mode 100644
index 0000000..289b6a7
--- /dev/null
+++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
@@ -0,0 +1,23 @@
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc('stock', 'doctype', 'quality_inspection_parameter')
+
+ # get all distinct parameters from QI readigs table
+ reading_params = frappe.db.get_all("Quality Inspection Reading", fields=["distinct specification"])
+ reading_params = [d.specification for d in reading_params]
+
+ # get all distinct parameters from QI Template as some may be unused in QI
+ template_params = frappe.db.get_all("Item Quality Inspection Parameter", fields=["distinct specification"])
+ template_params = [d.specification for d in template_params]
+
+ params = list(set(reading_params + template_params))
+
+ for parameter in params:
+ if not frappe.db.exists("Quality Inspection Parameter", parameter):
+ frappe.get_doc({
+ "doctype": "Quality Inspection Parameter",
+ "parameter": parameter,
+ "description": parameter
+ }).insert(ignore_permissions=True)
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 9388e09..e5a8a71 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -830,56 +830,49 @@
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
for supplier in suppliers:
- po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
- if len(po) == 0:
- doc = get_mapped_doc("Sales Order", source_name, {
- "Sales Order": {
- "doctype": "Purchase Order",
- "field_no_map": [
- "address_display",
- "contact_display",
- "contact_mobile",
- "contact_email",
- "contact_person",
- "taxes_and_charges",
- "shipping_address",
- "terms"
- ],
- "validation": {
- "docstatus": ["=", 1]
- }
- },
- "Sales Order Item": {
- "doctype": "Purchase Order Item",
- "field_map": [
- ["name", "sales_order_item"],
- ["parent", "sales_order"],
- ["stock_uom", "stock_uom"],
- ["uom", "uom"],
- ["conversion_factor", "conversion_factor"],
- ["delivery_date", "schedule_date"]
- ],
- "field_no_map": [
- "rate",
- "price_list_rate",
- "item_tax_template",
- "discount_percentage",
- "discount_amount",
- "pricing_rules"
- ],
- "postprocess": update_item,
- "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
+ doc = get_mapped_doc("Sales Order", source_name, {
+ "Sales Order": {
+ "doctype": "Purchase Order",
+ "field_no_map": [
+ "address_display",
+ "contact_display",
+ "contact_mobile",
+ "contact_email",
+ "contact_person",
+ "taxes_and_charges",
+ "shipping_address",
+ "terms"
+ ],
+ "validation": {
+ "docstatus": ["=", 1]
}
- }, target_doc, set_missing_values)
+ },
+ "Sales Order Item": {
+ "doctype": "Purchase Order Item",
+ "field_map": [
+ ["name", "sales_order_item"],
+ ["parent", "sales_order"],
+ ["stock_uom", "stock_uom"],
+ ["uom", "uom"],
+ ["conversion_factor", "conversion_factor"],
+ ["delivery_date", "schedule_date"]
+ ],
+ "field_no_map": [
+ "rate",
+ "price_list_rate",
+ "item_tax_template",
+ "discount_percentage",
+ "discount_amount",
+ "pricing_rules"
+ ],
+ "postprocess": update_item,
+ "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
+ }
+ }, target_doc, set_missing_values)
- doc.insert()
- else:
- suppliers =[]
- if suppliers:
+ doc.insert()
frappe.db.commit()
return doc
- else:
- frappe.msgprint(_("Purchase Order already created for all Sales Order items"))
@frappe.whitelist()
def make_purchase_order(source_name, selected_items=None, target_doc=None):
@@ -1094,4 +1087,4 @@
if not total_produced_qty and frappe.flags.in_patch: return
- frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty)
\ No newline at end of file
+ frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty)
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 643e7cf..e259367 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -772,6 +772,59 @@
so.load_from_db()
so.cancel()
+ def test_drop_shipping_partial_order(self):
+ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier, \
+ update_status as so_update_status
+
+ # make items
+ po_item1 = make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1})
+ po_item2 = make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1})
+
+ so_items = [
+ {
+ "item_code": po_item1.item_code,
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ },
+ {
+ "item_code": po_item2.item_code,
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ }
+ ]
+
+ # create so and po
+ so = make_sales_order(item_list=so_items, do_not_submit=True)
+ so.submit()
+
+ # create po for only one item
+ po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
+ po1.submit()
+
+ self.assertEqual(so.customer, po1.customer)
+ self.assertEqual(po1.items[0].sales_order, so.name)
+ self.assertEqual(po1.items[0].item_code, po_item1.item_code)
+ #test po item length
+ self.assertEqual(len(po1.items), 1)
+
+ # create po for remaining item
+ po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]])
+ po2.submit()
+
+ # teardown
+ so_update_status("Draft", so.name)
+
+ po1.cancel()
+ po2.cancel()
+ so.load_from_db()
+ so.cancel()
+
def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"])
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..3e81619 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,26 +8,32 @@
"field_order": [
"specification",
"value",
+ "non_numeric",
"column_break_3",
+ "min_value",
+ "max_value",
+ "formula_based_criteria",
"acceptance_formula"
],
"fields": [
{
"fieldname": "specification",
- "fieldtype": "Data",
+ "fieldtype": "Link",
"in_list_view": 1,
"label": "Parameter",
"oldfieldname": "specification",
"oldfieldtype": "Data",
+ "options": "Quality Inspection Parameter",
"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 +42,45 @@
"fieldtype": "Column Break"
},
{
- "description": "Simple Python formula based on numeric Readings.<br> Example 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 < 10</b>",
+ "depends_on": "formula_based_criteria",
+ "description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nNumeric eg. 2: <b>mean > 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": "2021-01-07 21:32:49.866439",
"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..f7565fd 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
@@ -4,6 +4,55 @@
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 +80,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 2084e3f..b3acbc5 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):
@@ -89,28 +87,78 @@
""".format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions),
(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 not reading.manual_inspection: # dont auto set status if manual
+ 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..8c5a04b 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,11 +124,20 @@
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}
+ if not args.readings:
+ create_quality_inspection_parameter("Size")
+ readings = {"specification": "Size", "min_value": 0, "max_value": 10}
+ else:
+ readings = args.readings
+
+ if args.status == "Rejected":
+ readings["reading_1"] = "12" # status is auto set in child on save
if isinstance(readings, list):
for entry in readings:
+ create_quality_inspection_parameter(entry["specification"])
qa.append("readings", entry)
else:
qa.append("readings", readings)
@@ -101,3 +148,11 @@
qa.submit()
return qa
+
+def create_quality_inspection_parameter(parameter):
+ if not frappe.db.exists("Quality Inspection Parameter", parameter):
+ frappe.get_doc({
+ "doctype": "Quality Inspection Parameter",
+ "parameter": parameter,
+ "description": parameter
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/__init__.py b/erpnext/stock/doctype/quality_inspection_parameter/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/__init__.py
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js
new file mode 100644
index 0000000..47c7e11
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Quality Inspection Parameter', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json
new file mode 100644
index 0000000..0b5a9b5
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json
@@ -0,0 +1,86 @@
+{
+ "actions": [],
+ "autoname": "field:parameter",
+ "creation": "2020-12-28 17:06:00.254129",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "parameter",
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "parameter",
+ "fieldtype": "Data",
+ "label": "Parameter",
+ "unique": 1
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "label": "Description"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-12-28 18:06:54.897317",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Quality Inspection Parameter",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Quality Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py
new file mode 100644
index 0000000..8678422
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class QualityInspectionParameter(Document):
+ pass
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py b/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py
new file mode 100644
index 0000000..cefdc08
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestQualityInspectionParameter(unittest.TestCase):
+ pass
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..dddb3d5 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,28 @@
"engine": "InnoDB",
"field_order": [
"specification",
- "value",
"status",
+ "value",
+ "non_numeric",
+ "manual_inspection",
"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"
],
@@ -29,19 +36,20 @@
{
"columns": 3,
"fieldname": "specification",
- "fieldtype": "Data",
+ "fieldtype": "Link",
"in_list_view": 1,
"label": "Parameter",
"oldfieldname": "specification",
"oldfieldtype": "Data",
+ "options": "Quality Inspection Parameter",
"reqd": 1
},
{
"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"
},
@@ -67,7 +75,6 @@
"columns": 1,
"fieldname": "reading_3",
"fieldtype": "Data",
- "in_list_view": 1,
"label": "Reading 3",
"oldfieldname": "reading_3",
"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 > 0.2 and reading_1 < 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 < 10</b>",
+ "depends_on": "formula_based_criteria",
+ "description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nNumeric eg. 2: <b>mean > 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,59 @@
{
"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",
+ "in_list_view": 1,
+ "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",
+ "in_list_view": 1,
+ "label": "Non-Numeric"
+ },
+ {
+ "default": "0",
+ "description": "Set the status manually.",
+ "fieldname": "manual_inspection",
+ "fieldtype": "Check",
+ "label": "Manual Inspection"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-16 16:34:29.947856",
+ "modified": "2021-01-07 21:56:40.235579",
"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