Merge branch 'develop' into batch-selector
This commit is contained in:
commit
aef71a8abe
@ -6,6 +6,7 @@ import unittest
|
||||
|
||||
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 @@ def make_quality_inspection_template():
|
||||
|
||||
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%",
|
||||
|
@ -23,8 +23,10 @@ class TestPatientAppointment(unittest.TestCase):
|
||||
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()
|
||||
|
@ -5,10 +5,10 @@ from __future__ import unicode_literals
|
||||
|
||||
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 @@ class TestTherapyPlan(unittest.TestCase):
|
||||
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()
|
||||
|
@ -47,7 +47,7 @@ class TherapyPlan(Document):
|
||||
|
||||
|
||||
@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 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company):
|
||||
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()
|
||||
|
@ -19,6 +19,15 @@ frappe.ui.form.on('Therapy Session', {
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query('appointment', function() {
|
||||
|
||||
return {
|
||||
filters: {
|
||||
'status': ['in', ['Open', 'Scheduled']]
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
@ -43,7 +43,14 @@ class TherapySession(Document):
|
||||
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):
|
||||
|
@ -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.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
|
||||
erpnext.patches.v13_0.set_company_in_leave_ledger_entry
|
||||
erpnext.patches.v13_0.convert_qi_parameter_to_link_field
|
||||
|
23
erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
Normal file
23
erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
Normal 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)
|
@ -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."))
|
||||
|
||||
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 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item):
|
||||
|
||||
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)
|
||||
|
@ -772,6 +772,59 @@ class TestSalesOrder(unittest.TestCase):
|
||||
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"])
|
||||
|
@ -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",
|
||||
|
@ -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 @@ 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');
|
||||
|
||||
},
|
||||
});
|
@ -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",
|
||||
|
@ -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):
|
||||
@ -89,28 +87,78 @@ class QualityInspection(Document):
|
||||
""".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
|
||||
|
@ -44,24 +44,61 @@ class TestQualityInspection(unittest.TestCase):
|
||||
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 @@ class TestQualityInspection(unittest.TestCase):
|
||||
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 @@ def create_quality_inspection(**args):
|
||||
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 @@ def create_quality_inspection(**args):
|
||||
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()
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
@ -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
|
||||
}
|
@ -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
|
@ -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
|
@ -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",
|
||||
|
@ -13,6 +13,7 @@ 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",
|
||||
"non_numeric", "formula_based_criteria", "min_value", "max_value"],
|
||||
filters={'parenttype': 'Quality Inspection Template', 'parent': template},
|
||||
order_by="idx")
|
Loading…
Reference in New Issue
Block a user