Merge branch 'develop' into mr-se-warehouse-validation

This commit is contained in:
Marica 2021-01-12 20:16:58 +05:30 committed by GitHub
commit b8c5cd7331
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1147 additions and 669 deletions

View File

@ -20,7 +20,8 @@ def get_data():
'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
},
{
'items': ['Item']
'label': _('Stock'),
'items': ['Item Groups', 'Item']
}
]
}

View File

@ -1699,6 +1699,7 @@ def get_mode_of_payment_info(mode_of_payment, company):
where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""",
(company, mode_of_payment), as_dict=1)
@frappe.whitelist()
def create_dunning(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount

View File

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

View File

@ -5,6 +5,7 @@ frappe.ui.form.on('Inpatient Medication Entry', {
refresh: function(frm) {
// Ignore cancellation of doctype on cancel all
frm.ignore_doctypes_on_cancel_all = ['Stock Entry'];
frm.fields_dict['medication_orders'].grid.wrapper.find('.grid-add-row').hide();
frm.set_query('item_code', () => {
return {

View File

@ -139,7 +139,6 @@
"fieldtype": "Table",
"label": "Inpatient Medication Orders",
"options": "Inpatient Medication Entry Detail",
"read_only": 1,
"reqd": 1
},
{
@ -180,7 +179,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-03 13:22:37.820707",
"modified": "2021-01-11 12:37:46.749659",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Entry",

View File

@ -15,8 +15,6 @@ class InpatientMedicationEntry(Document):
self.validate_medication_orders()
def get_medication_orders(self):
self.validate_datetime_filters()
# pull inpatient medication orders based on selected filters
orders = get_pending_medication_orders(self)
@ -27,22 +25,6 @@ class InpatientMedicationEntry(Document):
self.set('medication_orders', [])
frappe.msgprint(_('No pending medication orders found for selected criteria'))
def validate_datetime_filters(self):
if self.from_date and self.to_date:
self.validate_from_to_dates('from_date', 'to_date')
if self.from_date and getdate(self.from_date) > getdate():
frappe.throw(_('From Date cannot be after the current date.'))
if self.to_date and getdate(self.to_date) > getdate():
frappe.throw(_('To Date cannot be after the current date.'))
if self.from_time and self.from_time > nowtime():
frappe.throw(_('From Time cannot be after the current time.'))
if self.to_time and self.to_time > nowtime():
frappe.throw(_('To Time cannot be after the current time.'))
def add_mo_to_table(self, orders):
# Add medication orders in the child table
self.set('medication_orders', [])

View File

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

View File

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

View File

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

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

View File

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

View File

@ -742,4 +742,6 @@ 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.update_project_template_tasks
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

@ -0,0 +1,44 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("projects", "doctype", "project_template")
frappe.reload_doc("projects", "doctype", "project_template_task")
frappe.reload_doc("projects", "doctype", "project_template")
frappe.reload_doc("projects", "doctype", "task")
for template_name in frappe.db.sql("""
select
name
from
`tabProject Template` """,
as_dict=1):
template = frappe.get_doc("Project Template", template_name.name)
replace_tasks = False
new_tasks = []
for task in template.tasks:
if task.subject:
replace_tasks = True
new_task = frappe.get_doc(dict(
doctype = "Task",
subject = task.subject,
start = task.start,
duration = task.duration,
task_weight = task.task_weight,
description = task.description,
is_template = 1
)).insert()
new_tasks.append(new_task)
if replace_tasks:
template.tasks = []
for tsk in new_tasks:
template.append("tasks", {
"task": tsk.name,
"subject": tsk.subject
})
template.save()

View File

@ -577,7 +577,7 @@ class SalarySlip(TransactionBase):
'default_amount': amount if not struct_row.get("is_additional_component") else 0,
'depends_on_payment_days' : struct_row.depends_on_payment_days,
'salary_component' : struct_row.salary_component,
'abbr' : struct_row.abbr,
'abbr' : struct_row.abbr or struct_row.get("salary_component_abbr"),
'additional_salary': additional_salary,
'do_not_include_in_total' : struct_row.do_not_include_in_total,
'is_tax_applicable': struct_row.is_tax_applicable,

View File

@ -13,6 +13,7 @@ from frappe.desk.reportview import get_match_cond
from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_email
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from frappe.model.document import Document
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
class Project(Document):
def get_feed(self):
@ -54,18 +55,65 @@ class Project(Document):
self.project_type = template.project_type
# create tasks from template
project_tasks = []
tmp_task_details = []
for task in template.tasks:
frappe.get_doc(dict(
template_task_details = frappe.get_doc("Task", task.task)
tmp_task_details.append(template_task_details)
task = self.create_task_from_template(template_task_details)
project_tasks.append(task)
self.dependency_mapping(tmp_task_details, project_tasks)
def create_task_from_template(self, task_details):
return frappe.get_doc(dict(
doctype = 'Task',
subject = task.subject,
subject = task_details.subject,
project = self.name,
status = 'Open',
exp_start_date = add_days(self.expected_start_date, task.start),
exp_end_date = add_days(self.expected_start_date, task.start + task.duration),
description = task.description,
task_weight = task.task_weight
exp_start_date = self.calculate_start_date(task_details),
exp_end_date = self.calculate_end_date(task_details),
description = task_details.description,
task_weight = task_details.task_weight,
type = task_details.type,
issue = task_details.issue,
is_group = task_details.is_group
)).insert()
def calculate_start_date(self, task_details):
self.start_date = add_days(self.expected_start_date, task_details.start)
self.start_date = update_if_holiday(self.holiday_list, self.start_date)
return self.start_date
def calculate_end_date(self, task_details):
self.end_date = add_days(self.start_date, task_details.duration)
return update_if_holiday(self.holiday_list, self.end_date)
def dependency_mapping(self, template_tasks, project_tasks):
for template_task in template_tasks:
project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0]
project_task = frappe.get_doc("Task", project_task.name)
self.check_depends_on_value(template_task, project_task, project_tasks)
self.check_for_parent_tasks(template_task, project_task, project_tasks)
def check_depends_on_value(self, template_task, project_task, project_tasks):
if template_task.get("depends_on") and not project_task.get("depends_on"):
for child_task in template_task.get("depends_on"):
child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks))
if len(corresponding_project_task):
project_task.append("depends_on",{
"task": corresponding_project_task[0].name
})
project_task.save()
def check_for_parent_tasks(self, template_task, project_task, project_tasks):
if template_task.get("parent_task") and not project_task.get("parent_task"):
parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject")
corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks))
if len(corresponding_project_task):
project_task.parent_task = corresponding_project_task[0].name
project_task.save()
def is_row_updated(self, row, existing_task_data, fields):
if self.get("__islocal") or not existing_task_data: return True
@ -493,3 +541,9 @@ def set_project_status(project, status):
project.status = status
project.save()
def update_if_holiday(holiday_list, date):
holiday_list = holiday_list or get_holiday_list()
while is_holiday(holiday_list, date):
date = add_days(date, 1)
return date

View File

@ -7,60 +7,129 @@ import frappe, unittest
test_records = frappe.get_test_records('Project')
test_ignore = ["Sales Order"]
from erpnext.projects.doctype.project_template.test_project_template import get_project_template, make_project_template
from erpnext.projects.doctype.project.project import set_project_status
from frappe.utils import getdate
from erpnext.projects.doctype.project_template.test_project_template import make_project_template
from erpnext.projects.doctype.project.project import update_if_holiday
from erpnext.projects.doctype.task.test_task import create_task
from frappe.utils import getdate, nowdate, add_days
class TestProject(unittest.TestCase):
def test_project_with_template(self):
frappe.db.sql('delete from tabTask where project = "Test Project with Template"')
frappe.delete_doc('Project', 'Test Project with Template')
def test_project_with_template_having_no_parent_and_depend_tasks(self):
project_name = "Test Project with Template - No Parent and Dependend Tasks"
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.delete_doc('Project', project_name)
project = get_project('Test Project with Template')
task1 = task_exists("Test Template Task with No Parent and Dependency")
if not task1:
task1 = create_task(subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3)
tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc')
template = make_project_template("Test Project Template - No Parent and Dependend Tasks", [task1])
project = get_project(project_name, template)
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks'], dict(project=project.name), order_by='creation asc')
task1 = tasks[0]
self.assertEqual(task1.subject, 'Task 1')
self.assertEqual(task1.description, 'Task 1 description')
self.assertEqual(getdate(task1.exp_start_date), getdate('2019-01-01'))
self.assertEqual(getdate(task1.exp_end_date), getdate('2019-01-04'))
self.assertEqual(tasks[0].subject, 'Test Template Task with No Parent and Dependency')
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3))
self.assertEqual(len(tasks), 1)
self.assertEqual(len(tasks), 4)
task4 = tasks[3]
self.assertEqual(task4.subject, 'Task 4')
self.assertEqual(getdate(task4.exp_end_date), getdate('2019-01-06'))
def test_project_template_having_parent_child_tasks(self):
project_name = "Test Project with Template - Tasks with Parent-Child Relation"
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.delete_doc('Project', project_name)
def get_project(name):
template = get_project_template()
task1 = task_exists("Test Template Task Parent")
if not task1:
task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=1)
task2 = task_exists("Test Template Task Child 1")
if not task2:
task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3)
task3 = task_exists("Test Template Task Child 2")
if not task3:
task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3)
template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3])
project = get_project(project_name, template)
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc')
self.assertEqual(tasks[0].subject, 'Test Template Task Parent')
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1))
self.assertEqual(tasks[1].subject, 'Test Template Task Child 1')
self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3))
self.assertEqual(tasks[1].parent_task, tasks[0].name)
self.assertEqual(tasks[2].subject, 'Test Template Task Child 2')
self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, 2, 3))
self.assertEqual(tasks[2].parent_task, tasks[0].name)
self.assertEqual(len(tasks), 3)
def test_project_template_having_dependent_tasks(self):
project_name = "Test Project with Template - Dependent Tasks"
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.delete_doc('Project', project_name)
task1 = task_exists("Test Template Task for Dependency")
if not task1:
task1 = create_task(subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1)
task2 = task_exists("Test Template Task with Dependency")
if not task2:
task2 = create_task(subject="Test Template Task with Dependency", depends_on=task1.name, is_template=1, begin=2, duration=2)
template = make_project_template("Test Project with Template - Dependent Tasks", [task1, task2])
project = get_project(project_name, template)
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc')
self.assertEqual(tasks[1].subject, 'Test Template Task with Dependency')
self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 2, 2))
self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 )
self.assertEqual(tasks[0].subject, 'Test Template Task for Dependency')
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1) )
self.assertEqual(len(tasks), 2)
def get_project(name, template):
project = frappe.get_doc(dict(
doctype = 'Project',
project_name = name,
status = 'Open',
project_template = template.name,
expected_start_date = '2019-01-01'
expected_start_date = nowdate()
)).insert()
return project
def make_project(args):
args = frappe._dict(args)
if args.project_template_name:
template = make_project_template(args.project_template_name)
else:
template = get_project_template()
project = frappe.get_doc(dict(
doctype = 'Project',
project_name = args.project_name,
status = 'Open',
project_template = template.name,
expected_start_date = args.start_date
))
if args.project_template_name:
template = make_project_template(args.project_template_name)
project.project_template = template.name
if not frappe.db.exists("Project", args.project_name):
project.insert()
return project
def task_exists(subject):
result = frappe.db.get_list("Task", filters={"subject": subject},fields=["name"])
if not len(result):
return False
return frappe.get_doc("Task", result[0].name)
def calculate_end_date(project, start, duration):
start = add_days(project.expected_start_date, start)
start = update_if_holiday(project.holiday_list, start)
end = add_days(start, duration)
end = update_if_holiday(project.holiday_list, end)
return getdate(end)

View File

@ -5,4 +5,23 @@ frappe.ui.form.on('Project Template', {
// refresh: function(frm) {
// }
setup: function (frm) {
frm.set_query("task", "tasks", function () {
return {
filters: {
"is_template": 1
}
};
});
}
});
frappe.ui.form.on('Project Template Task', {
task: function (frm, cdt, cdn) {
var row = locals[cdt][cdn];
frappe.db.get_value("Task", row.task, "subject", (value) => {
row.subject = value.subject;
refresh_field("tasks");
});
}
});

View File

@ -3,8 +3,28 @@
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import get_link_to_form
class ProjectTemplate(Document):
pass
def validate(self):
self.validate_dependencies()
def validate_dependencies(self):
for task in self.tasks:
task_details = frappe.get_doc("Task", task.task)
if task_details.depends_on:
for dependency_task in task_details.depends_on:
if not self.check_dependent_task_presence(dependency_task.task):
task_details_format = get_link_to_form("Task",task_details.name)
dependency_task_format = get_link_to_form("Task", dependency_task.task)
frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format)))
def check_dependent_task_presence(self, task):
for task_details in self.tasks:
if task_details.task == task:
return True
return False

View File

@ -5,44 +5,25 @@ from __future__ import unicode_literals
import frappe
import unittest
from erpnext.projects.doctype.task.test_task import create_task
class TestProjectTemplate(unittest.TestCase):
pass
def get_project_template():
if not frappe.db.exists('Project Template', 'Test Project Template'):
frappe.get_doc(dict(
doctype = 'Project Template',
name = 'Test Project Template',
tasks = [
dict(subject='Task 1', description='Task 1 description',
start=0, duration=3),
dict(subject='Task 2', description='Task 2 description',
start=0, duration=2),
dict(subject='Task 3', description='Task 3 description',
start=2, duration=4),
dict(subject='Task 4', description='Task 4 description',
start=3, duration=2),
]
)).insert()
return frappe.get_doc('Project Template', 'Test Project Template')
def make_project_template(project_template_name, project_tasks=[]):
if not frappe.db.exists('Project Template', project_template_name):
frappe.get_doc(dict(
doctype = 'Project Template',
name = project_template_name,
tasks = project_tasks or [
dict(subject='Task 1', description='Task 1 description',
start=0, duration=3),
dict(subject='Task 2', description='Task 2 description',
start=0, duration=2),
dict(subject='Task 3', description='Task 3 description',
start=2, duration=4),
dict(subject='Task 4', description='Task 4 description',
start=3, duration=2),
project_tasks = project_tasks or [
create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3),
create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2),
]
)).insert()
doc = frappe.get_doc(dict(
doctype = 'Project Template',
name = project_template_name
))
for task in project_tasks:
doc.append("tasks",{
"task": task.name
})
doc.insert()
return frappe.get_doc('Project Template', project_template_name)

View File

@ -1,203 +1,41 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"actions": [],
"creation": "2019-02-18 17:24:41.830096",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"task",
"subject"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"columns": 2,
"fieldname": "task",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Task",
"options": "Task",
"reqd": 1
},
{
"columns": 6,
"fieldname": "subject",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"fieldtype": "Read Only",
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Subject",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "start",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Begin On (Days)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "duration",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Duration (Days)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "task_weight",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Task Weight",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Subject"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2019-02-18 18:30:22.688966",
"links": [],
"modified": "2021-01-07 15:13:40.995071",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Template Task",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View File

@ -12,6 +12,7 @@
"issue",
"type",
"is_group",
"is_template",
"column_break0",
"status",
"priority",
@ -22,9 +23,11 @@
"sb_timeline",
"exp_start_date",
"expected_time",
"start",
"column_break_11",
"exp_end_date",
"progress",
"duration",
"is_milestone",
"sb_details",
"description",
@ -112,7 +115,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "Open\nWorking\nPending Review\nOverdue\nCompleted\nCancelled"
"options": "Open\nWorking\nPending Review\nOverdue\nTemplate\nCompleted\nCancelled"
},
{
"fieldname": "priority",
@ -360,6 +363,24 @@
"label": "Completed By",
"no_copy": 1,
"options": "User"
},
{
"default": "0",
"fieldname": "is_template",
"fieldtype": "Check",
"label": "Is Template"
},
{
"depends_on": "is_template",
"fieldname": "start",
"fieldtype": "Int",
"label": "Begin On (Days)"
},
{
"depends_on": "is_template",
"fieldname": "duration",
"fieldtype": "Int",
"label": "Duration (Days)"
}
],
"icon": "fa fa-check",
@ -367,7 +388,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 5,
"modified": "2020-07-03 12:36:04.960457",
"modified": "2020-12-28 11:32:58.714991",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",

View File

@ -34,6 +34,7 @@ class Task(NestedSet):
self.validate_progress()
self.validate_status()
self.update_depends_on()
self.validate_dependencies_for_template_task()
def validate_dates(self):
if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
@ -55,6 +56,8 @@ class Task(NestedSet):
validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
def validate_status(self):
if self.is_template and self.status != "Template":
self.status = "Template"
if self.status!=self.get_db_value("status") and self.status == "Completed":
for d in self.depends_on:
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
@ -72,10 +75,28 @@ class Task(NestedSet):
if self.status == 'Completed':
self.progress = 100
def validate_dependencies_for_template_task(self):
if self.is_template:
self.validate_parent_template_task()
self.validate_depends_on_tasks()
def validate_parent_template_task(self):
if self.parent_task:
if not frappe.db.get_value("Task", self.parent_task, "is_template"):
parent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(self.parent_task)
frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
def validate_depends_on_tasks(self):
if self.depends_on:
for task in self.depends_on:
if not frappe.db.get_value("Task", task.task, "is_template"):
dependent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(task.task)
frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
def update_depends_on(self):
depends_on_tasks = self.depends_on_tasks or ""
for d in self.depends_on:
if d.task and not d.task in depends_on_tasks:
if d.task and d.task not in depends_on_tasks:
depends_on_tasks += d.task + ","
self.depends_on_tasks = depends_on_tasks
@ -161,7 +182,7 @@ class Task(NestedSet):
def populate_depends_on(self):
if self.parent_task:
parent = frappe.get_doc('Task', self.parent_task)
if not self.name in [row.task for row in parent.depends_on]:
if self.name not in [row.task for row in parent.depends_on]:
parent.append("depends_on", {
"doctype": "Task Depends On",
"task": self.name,

View File

@ -20,7 +20,8 @@ frappe.listview_settings['Task'] = {
"Pending Review": "orange",
"Working": "orange",
"Completed": "green",
"Cancelled": "dark grey"
"Cancelled": "dark grey",
"Template": "blue"
}
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
},

View File

@ -97,14 +97,19 @@ class TestTask(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue")
def create_task(subject, start=None, end=None, depends_on=None, project=None, save=True):
def create_task(subject, start=None, end=None, depends_on=None, project=None, parent_task=None, is_group=0, is_template=0, begin=0, duration=0, save=True):
if not frappe.db.exists("Task", subject):
task = frappe.new_doc('Task')
task.status = "Open"
task.subject = subject
task.exp_start_date = start or nowdate()
task.exp_end_date = end or nowdate()
task.project = project or "_Test Project"
task.project = project or None if is_template else "_Test Project"
task.is_template = is_template
task.start = begin
task.duration = duration
task.is_group = is_group
task.parent_task = parent_task
if save:
task.save()
else:
@ -116,5 +121,4 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, sa
})
if save:
task.save()
return task

View File

@ -25,7 +25,6 @@ class Quotation(SellingController):
def validate(self):
super(Quotation, self).validate()
self.set_status()
self.update_opportunity()
self.validate_uom_is_integer("stock_uom", "qty")
self.validate_valid_till()
self.set_customer_name()
@ -50,21 +49,20 @@ class Quotation(SellingController):
lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"])
self.customer_name = company_name or lead_name
def update_opportunity(self):
def update_opportunity(self, status):
for opportunity in list(set([d.prevdoc_docname for d in self.get("items")])):
if opportunity:
self.update_opportunity_status(opportunity)
self.update_opportunity_status(status, opportunity)
if self.opportunity:
self.update_opportunity_status()
self.update_opportunity_status(status)
def update_opportunity_status(self, opportunity=None):
def update_opportunity_status(self, status, opportunity=None):
if not opportunity:
opportunity = self.opportunity
opp = frappe.get_doc("Opportunity", opportunity)
opp.status = None
opp.set_status(update=True)
opp.set_status(status=status, update=True)
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_sales_order():
@ -82,7 +80,7 @@ class Quotation(SellingController):
else:
frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason'))))
self.update_opportunity()
self.update_opportunity('Lost')
self.update_lead()
self.save()
@ -95,7 +93,7 @@ class Quotation(SellingController):
self.company, self.base_grand_total, self)
#update enquiry status
self.update_opportunity()
self.update_opportunity('Quotation')
self.update_lead()
def on_cancel(self):
@ -105,7 +103,7 @@ class Quotation(SellingController):
#update enquiry status
self.set_status(update=True)
self.update_opportunity()
self.update_opportunity('Open')
self.update_lead()
def print_other_charges(self,docname):

View File

@ -158,7 +158,6 @@ class SalesOrder(SellingController):
frappe.throw(_("Quotation {0} is cancelled").format(quotation))
doc.set_status(update=True)
doc.update_opportunity()
def validate_drop_ship(self):
for d in self.get('items'):
@ -830,8 +829,6 @@ 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",
@ -873,13 +870,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
}, target_doc, set_missing_values)
doc.insert()
else:
suppliers =[]
if suppliers:
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):

View File

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

View File

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

View File

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

View File

@ -136,6 +136,7 @@
"width": "50%"
},
{
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
@ -143,6 +144,7 @@
"read_only": 1
},
{
"fetch_from": "item_code.description",
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
@ -236,7 +238,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-19 17:06:05.409963",
"modified": "2020-12-18 19:59:55.710300",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",

View File

@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe import _
from frappe.utils import flt
from frappe.utils import flt, cint
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \
import get_template_details
@ -16,7 +16,7 @@ class QualityInspection(Document):
self.get_item_specification_details()
if self.readings:
self.set_status_based_on_acceptance_formula()
self.inspect_and_set_status()
def get_item_specification_details(self):
if not self.quality_inspection_template:
@ -29,9 +29,7 @@ class QualityInspection(Document):
parameters = get_template_details(self.quality_inspection_template)
for d in parameters:
child = self.append('readings', {})
child.specification = d.specification
child.value = d.value
child.acceptance_formula = d.acceptance_formula
child.update(d)
child.status = "Accepted"
def get_quality_inspection_template(self):
@ -69,35 +67,98 @@ class QualityInspection(Document):
doctype = 'Stock Entry Detail'
if self.reference_type and self.reference_name:
conditions = ""
if self.batch_no and self.docstatus == 1:
conditions += " and t1.batch_no = '%s'"%(self.batch_no)
if self.docstatus == 2: # if cancel, then remove qi link wherever same name
conditions += " and t1.quality_inspection = '%s'"%(self.name)
frappe.db.sql("""
UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2
SET t1.quality_inspection = %s, t2.modified = %s
WHERE t1.parent = %s and t1.item_code = %s and t1.parent = t2.name
""".format(parent_doc=self.reference_type, child_doc=doctype),
UPDATE
`tab{child_doc}` t1, `tab{parent_doc}` t2
SET
t1.quality_inspection = %s, t2.modified = %s
WHERE
t1.parent = %s
and t1.item_code = %s
and t1.parent = t2.name
{conditions}
""".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)
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 = {}
for i in range(1, 11):
field = "reading_" + str(i)
data[field] = flt(reading.get(field)) or 0
data = self.get_formula_evaluation_data(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"))
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))
data["mean"] = self.calculate_mean(reading)
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

View File

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

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",
"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"
@ -130,18 +137,21 @@
"label": "Status",
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "Accepted\nRejected"
"options": "\nAccepted\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 &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",
"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 22:16:53.978410",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection Reading",

View File

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