Merge branch 'develop' into batch-selector

This commit is contained in:
Marica 2021-01-11 14:22:11 +05:30 committed by GitHub
commit aef71a8abe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 563 additions and 152 deletions

View File

@ -6,6 +6,7 @@ import unittest
from erpnext.stock.doctype.item.test_item import set_item_variant_settings 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.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 from six import string_types
@ -56,6 +57,8 @@ def make_quality_inspection_template():
qc = frappe.new_doc("Quality Inspection Template") qc = frappe.new_doc("Quality Inspection Template")
qc.quality_inspection_template_name = qc_template qc.quality_inspection_template_name = qc_template
create_quality_inspection_parameter("Moisture")
qc.append('item_quality_inspection_parameter', { qc.append('item_quality_inspection_parameter', {
"specification": "Moisture", "specification": "Moisture",
"value": "< 5%", "value": "< 5%",

View File

@ -23,8 +23,10 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEquals(appointment.status, 'Open') self.assertEquals(appointment.status, 'Open')
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2)) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2))
self.assertEquals(appointment.status, 'Scheduled') self.assertEquals(appointment.status, 'Scheduled')
create_encounter(appointment) encounter = create_encounter(appointment)
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed') 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): def test_start_encounter(self):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()

View File

@ -5,10 +5,10 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest 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_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.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): class TestTherapyPlan(unittest.TestCase):
def test_creation_on_encounter_submission(self): def test_creation_on_encounter_submission(self):
@ -28,6 +28,15 @@ class TestTherapyPlan(unittest.TestCase):
frappe.get_doc(session).submit() frappe.get_doc(session).submit()
self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed') 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): def test_therapy_plan_from_template(self):
patient = create_patient() patient = create_patient()
template = create_therapy_plan_template() template = create_therapy_plan_template()

View File

@ -47,7 +47,7 @@ class TherapyPlan(Document):
@frappe.whitelist() @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_type = frappe.get_doc('Therapy Type', therapy_type)
therapy_session = frappe.new_doc('Therapy Session') therapy_session = frappe.new_doc('Therapy Session')
@ -58,6 +58,7 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company):
therapy_session.duration = therapy_type.default_duration therapy_session.duration = therapy_type.default_duration
therapy_session.rate = therapy_type.rate therapy_session.rate = therapy_type.rate
therapy_session.exercises = therapy_type.exercises therapy_session.exercises = therapy_type.exercises
therapy_session.appointment = appointment
if frappe.flags.in_test: if frappe.flags.in_test:
therapy_session.start_date = today() therapy_session.start_date = today()

View File

@ -19,6 +19,15 @@ frappe.ui.form.on('Therapy Session', {
} }
}; };
}); });
frm.set_query('appointment', function() {
return {
filters: {
'status': ['in', ['Open', 'Scheduled']]
}
};
});
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -43,7 +43,14 @@ class TherapySession(Document):
self.update_sessions_count_in_therapy_plan() self.update_sessions_count_in_therapy_plan()
insert_session_medical_record(self) 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): 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) self.update_sessions_count_in_therapy_plan(on_cancel=True)
def update_sessions_count_in_therapy_plan(self, on_cancel=False): def update_sessions_count_in_therapy_plan(self, on_cancel=False):

View File

@ -742,4 +742,5 @@ erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy 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.add_po_to_global_search
erpnext.patches.v13_0.update_returned_qty_in_pr_dn erpnext.patches.v13_0.update_returned_qty_in_pr_dn
erpnext.patches.v13_0.set_company_in_leave_ledger_entry erpnext.patches.v13_0.set_company_in_leave_ledger_entry
erpnext.patches.v13_0.convert_qi_parameter_to_link_field

View File

@ -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)

View File

@ -830,56 +830,49 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
for supplier in suppliers: for supplier in suppliers:
po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) doc = get_mapped_doc("Sales Order", source_name, {
if len(po) == 0: "Sales Order": {
doc = get_mapped_doc("Sales Order", source_name, { "doctype": "Purchase Order",
"Sales Order": { "field_no_map": [
"doctype": "Purchase Order", "address_display",
"field_no_map": [ "contact_display",
"address_display", "contact_mobile",
"contact_display", "contact_email",
"contact_mobile", "contact_person",
"contact_email", "taxes_and_charges",
"contact_person", "shipping_address",
"taxes_and_charges", "terms"
"shipping_address", ],
"terms" "validation": {
], "docstatus": ["=", 1]
"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
} }
}, 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() doc.insert()
else:
suppliers =[]
if suppliers:
frappe.db.commit() frappe.db.commit()
return doc return doc
else:
frappe.msgprint(_("Purchase Order already created for all Sales Order items"))
@frappe.whitelist() @frappe.whitelist()
def make_purchase_order(source_name, selected_items=None, target_doc=None): def make_purchase_order(source_name, selected_items=None, target_doc=None):
@ -1094,4 +1087,4 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item):
if not total_produced_qty and frappe.flags.in_patch: return 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) frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty)

View File

@ -772,6 +772,59 @@ class TestSalesOrder(unittest.TestCase):
so.load_from_db() so.load_from_db()
so.cancel() 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): def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"]) fields=["reserved_qty"])

View File

@ -8,26 +8,32 @@
"field_order": [ "field_order": [
"specification", "specification",
"value", "value",
"non_numeric",
"column_break_3", "column_break_3",
"min_value",
"max_value",
"formula_based_criteria",
"acceptance_formula" "acceptance_formula"
], ],
"fields": [ "fields": [
{ {
"fieldname": "specification", "fieldname": "specification",
"fieldtype": "Data", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Parameter", "label": "Parameter",
"oldfieldname": "specification", "oldfieldname": "specification",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "Quality Inspection Parameter",
"print_width": "200px", "print_width": "200px",
"reqd": 1, "reqd": 1,
"width": "200px" "width": "100px"
}, },
{ {
"depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
"fieldname": "value", "fieldname": "value",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Acceptance Criteria", "label": "Acceptance Criteria Value",
"oldfieldname": "value", "oldfieldname": "value",
"oldfieldtype": "Data" "oldfieldtype": "Data"
}, },
@ -36,17 +42,45 @@
"fieldtype": "Column Break" "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", "fieldname": "acceptance_formula",
"fieldtype": "Code", "fieldtype": "Code",
"in_list_view": 1,
"label": "Acceptance Criteria Formula" "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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-11-16 16:33:42.421842", "modified": "2021-01-07 21:32:49.866439",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Quality Inspection Parameter", "name": "Item Quality Inspection Parameter",

View File

@ -4,6 +4,55 @@
cur_frm.cscript.refresh = cur_frm.cscript.inspection_type; cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
frappe.ui.form.on("Quality Inspection", { 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) { refresh: function(frm) {
// Ignore cancellation of reference doctype on cancel all. // Ignore cancellation of reference doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type]; frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type];
@ -31,55 +80,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%" "width": "50%"
}, },
{ {
"fetch_from": "item_code.item_name",
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_global_search": 1, "in_global_search": 1,
@ -143,6 +144,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "item_code.description",
"fieldname": "description", "fieldname": "description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Description", "label": "Description",
@ -236,7 +238,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-19 17:06:05.409963", "modified": "2020-12-18 19:59:55.710300",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Quality Inspection", "name": "Quality Inspection",

View File

@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt, cint
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \ from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \
import get_template_details import get_template_details
@ -16,7 +16,7 @@ class QualityInspection(Document):
self.get_item_specification_details() self.get_item_specification_details()
if self.readings: if self.readings:
self.set_status_based_on_acceptance_formula() self.inspect_and_set_status()
def get_item_specification_details(self): def get_item_specification_details(self):
if not self.quality_inspection_template: if not self.quality_inspection_template:
@ -29,9 +29,7 @@ class QualityInspection(Document):
parameters = get_template_details(self.quality_inspection_template) parameters = get_template_details(self.quality_inspection_template)
for d in parameters: for d in parameters:
child = self.append('readings', {}) child = self.append('readings', {})
child.specification = d.specification child.update(d)
child.value = d.value
child.acceptance_formula = d.acceptance_formula
child.status = "Accepted" child.status = "Accepted"
def get_quality_inspection_template(self): def get_quality_inspection_template(self):
@ -89,28 +87,78 @@ class QualityInspection(Document):
""".format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions), """.format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions),
(quality_inspection, self.modified, self.reference_name, self.item_code)) (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: 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 def set_status_based_on_acceptance_values(self, reading):
data = {} 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): for i in range(1, 11):
field = "reading_" + str(i) 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: return data
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"))
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.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs

View File

@ -44,24 +44,61 @@ class TestQualityInspection(unittest.TestCase):
qa.delete() qa.delete()
dn.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): def test_formula_based_qi_readings(self):
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
readings = [{ readings = [{
"specification": "Iron Content", "specification": "Iron Content", # numeric reading
"formula_based_criteria": 1,
"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", "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", "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
"reading_1": 0.7 "reading_1": "0.7"
}, },
{ {
"specification": "Mg Content", "specification": "Mg Content", # numeric reading
"acceptance_formula": "(reading_1 + reading_2 + reading_3) / 3 < 0.9", "formula_based_criteria": 1,
"reading_1": 0.5, "acceptance_formula": "mean < 0.9",
"reading_2": 0.7, "reading_1": "0.5",
"reading_2": "0.7",
"reading_3": "random text" # check if random string input causes issues "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, qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
@ -72,6 +109,7 @@ class TestQualityInspection(unittest.TestCase):
self.assertEqual(qa.readings[0].status, "Accepted") self.assertEqual(qa.readings[0].status, "Accepted")
self.assertEqual(qa.readings[1].status, "Rejected") self.assertEqual(qa.readings[1].status, "Rejected")
self.assertEqual(qa.readings[2].status, "Accepted") self.assertEqual(qa.readings[2].status, "Accepted")
self.assertEqual(qa.readings[3].status, "Accepted")
qa.delete() qa.delete()
dn.delete() dn.delete()
@ -86,11 +124,20 @@ def create_quality_inspection(**args):
qa.item_code = args.item_code or "_Test Item with QA" qa.item_code = args.item_code or "_Test Item with QA"
qa.sample_size = 1 qa.sample_size = 1
qa.inspected_by = frappe.session.user 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): if isinstance(readings, list):
for entry in readings: for entry in readings:
create_quality_inspection_parameter(entry["specification"])
qa.append("readings", entry) qa.append("readings", entry)
else: else:
qa.append("readings", readings) qa.append("readings", readings)
@ -101,3 +148,11 @@ def create_quality_inspection(**args):
qa.submit() qa.submit()
return qa 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()

View File

@ -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) {
// }
});

View File

@ -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
}

View File

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

View File

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

View File

@ -7,21 +7,28 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"specification", "specification",
"value",
"status", "status",
"value",
"non_numeric",
"manual_inspection",
"column_break_4", "column_break_4",
"min_value",
"max_value",
"formula_based_criteria",
"acceptance_formula", "acceptance_formula",
"section_break_3", "section_break_3",
"reading_value",
"section_break_14",
"reading_1", "reading_1",
"reading_2", "reading_2",
"reading_3", "reading_3",
"column_break_10",
"reading_4", "reading_4",
"column_break_10",
"reading_5", "reading_5",
"reading_6", "reading_6",
"column_break_14",
"reading_7", "reading_7",
"reading_8", "reading_8",
"column_break_14",
"reading_9", "reading_9",
"reading_10" "reading_10"
], ],
@ -29,19 +36,20 @@
{ {
"columns": 3, "columns": 3,
"fieldname": "specification", "fieldname": "specification",
"fieldtype": "Data", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Parameter", "label": "Parameter",
"oldfieldname": "specification", "oldfieldname": "specification",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"options": "Quality Inspection Parameter",
"reqd": 1 "reqd": 1
}, },
{ {
"columns": 2, "columns": 2,
"depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)",
"fieldname": "value", "fieldname": "value",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "label": "Acceptance Criteria Value",
"label": "Acceptance Criteria",
"oldfieldname": "value", "oldfieldname": "value",
"oldfieldtype": "Data" "oldfieldtype": "Data"
}, },
@ -67,7 +75,6 @@
"columns": 1, "columns": 1,
"fieldname": "reading_3", "fieldname": "reading_3",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1,
"label": "Reading 3", "label": "Reading 3",
"oldfieldname": "reading_3", "oldfieldname": "reading_3",
"oldfieldtype": "Data" "oldfieldtype": "Data"
@ -133,15 +140,18 @@
"options": "Accepted\nRejected" "options": "Accepted\nRejected"
}, },
{ {
"depends_on": "non_numeric",
"fieldname": "section_break_3", "fieldname": "section_break_3",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Value Based Inspection"
}, },
{ {
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Column Break" "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", "fieldname": "acceptance_formula",
"fieldtype": "Code", "fieldtype": "Code",
"label": "Acceptance Criteria Formula" "label": "Acceptance Criteria Formula"
@ -153,12 +163,59 @@
{ {
"fieldname": "column_break_14", "fieldname": "column_break_14",
"fieldtype": "Column Break" "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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-11-16 16:34:29.947856", "modified": "2021-01-07 21:56:40.235579",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Quality Inspection Reading", "name": "Quality Inspection Reading",

View File

@ -13,6 +13,7 @@ def get_template_details(template):
if not template: return [] if not template: return []
return frappe.get_all('Item Quality Inspection Parameter', 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}, filters={'parenttype': 'Quality Inspection Template', 'parent': template},
order_by="idx") order_by="idx")