Merge branch 'develop' into mr-se-warehouse-validation
This commit is contained in:
commit
b8c5cd7331
@ -20,7 +20,8 @@ def get_data():
|
||||
'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
|
||||
},
|
||||
{
|
||||
'items': ['Item']
|
||||
'label': _('Stock'),
|
||||
'items': ['Item Groups', 'Item']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -179,7 +179,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
# this sequence because outstanding may get -ve
|
||||
self.make_gl_entries()
|
||||
|
||||
|
||||
if self.update_stock == 1:
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
@ -261,10 +261,10 @@ class SalesInvoice(SellingController):
|
||||
self.update_stock_ledger()
|
||||
|
||||
self.make_gl_entries_on_cancel()
|
||||
|
||||
|
||||
if self.update_stock == 1:
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
|
||||
frappe.db.set(self, 'status', 'Cancelled')
|
||||
|
||||
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
|
||||
@ -551,7 +551,7 @@ class SalesInvoice(SellingController):
|
||||
def add_remarks(self):
|
||||
if not self.remarks:
|
||||
if self.po_no and self.po_date:
|
||||
self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
|
||||
self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
|
||||
formatdate(self.po_date))
|
||||
else:
|
||||
self.remarks = _("No Remarks")
|
||||
@ -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
|
||||
|
@ -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%",
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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', [])
|
||||
|
@ -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,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.set_company_in_leave_ledger_entry
|
||||
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
|
||||
|
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)
|
44
erpnext/patches/v13_0/update_project_template_tasks.py
Normal file
44
erpnext/patches/v13_0/update_project_template_tasks.py
Normal 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()
|
@ -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,
|
||||
|
@ -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,17 +55,64 @@ 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(
|
||||
doctype = 'Task',
|
||||
subject = task.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
|
||||
)).insert()
|
||||
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_details.subject,
|
||||
project = self.name,
|
||||
status = 'Open',
|
||||
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
|
||||
|
@ -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
|
||||
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)
|
@ -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");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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)
|
@ -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
|
||||
}
|
@ -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",
|
||||
|
@ -17,291 +17,312 @@ class CircularReferenceError(frappe.ValidationError): pass
|
||||
class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass
|
||||
|
||||
class Task(NestedSet):
|
||||
nsm_parent_field = 'parent_task'
|
||||
nsm_parent_field = 'parent_task'
|
||||
|
||||
def get_feed(self):
|
||||
return '{0}: {1}'.format(_(self.status), self.subject)
|
||||
def get_feed(self):
|
||||
return '{0}: {1}'.format(_(self.status), self.subject)
|
||||
|
||||
def get_customer_details(self):
|
||||
cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
|
||||
if cust:
|
||||
ret = {'customer_name': cust and cust[0][0] or ''}
|
||||
return ret
|
||||
def get_customer_details(self):
|
||||
cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
|
||||
if cust:
|
||||
ret = {'customer_name': cust and cust[0][0] or ''}
|
||||
return ret
|
||||
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
self.validate_parent_project_dates()
|
||||
self.validate_progress()
|
||||
self.validate_status()
|
||||
self.update_depends_on()
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
self.validate_parent_project_dates()
|
||||
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):
|
||||
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
|
||||
frappe.bold("Expected End Date")))
|
||||
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):
|
||||
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
|
||||
frappe.bold("Expected End Date")))
|
||||
|
||||
if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
|
||||
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
|
||||
frappe.bold("Actual End Date")))
|
||||
if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
|
||||
frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
|
||||
frappe.bold("Actual End Date")))
|
||||
|
||||
def validate_parent_project_dates(self):
|
||||
if not self.project or frappe.flags.in_test:
|
||||
return
|
||||
def validate_parent_project_dates(self):
|
||||
if not self.project or frappe.flags.in_test:
|
||||
return
|
||||
|
||||
expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
|
||||
expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
|
||||
|
||||
if expected_end_date:
|
||||
validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
|
||||
validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
|
||||
if expected_end_date:
|
||||
validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
|
||||
validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
|
||||
|
||||
def validate_status(self):
|
||||
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"):
|
||||
frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
|
||||
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"):
|
||||
frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
|
||||
|
||||
close_all_assignments(self.doctype, self.name)
|
||||
close_all_assignments(self.doctype, self.name)
|
||||
|
||||
def validate_progress(self):
|
||||
if flt(self.progress or 0) > 100:
|
||||
frappe.throw(_("Progress % for a task cannot be more than 100."))
|
||||
def validate_progress(self):
|
||||
if flt(self.progress or 0) > 100:
|
||||
frappe.throw(_("Progress % for a task cannot be more than 100."))
|
||||
|
||||
if flt(self.progress) == 100:
|
||||
self.status = 'Completed'
|
||||
if flt(self.progress) == 100:
|
||||
self.status = 'Completed'
|
||||
|
||||
if self.status == 'Completed':
|
||||
self.progress = 100
|
||||
if self.status == 'Completed':
|
||||
self.progress = 100
|
||||
|
||||
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:
|
||||
depends_on_tasks += d.task + ","
|
||||
self.depends_on_tasks = depends_on_tasks
|
||||
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_nsm_model(self):
|
||||
frappe.utils.nestedset.update_nsm(self)
|
||||
def update_depends_on(self):
|
||||
depends_on_tasks = self.depends_on_tasks or ""
|
||||
for d in self.depends_on:
|
||||
if d.task and d.task not in depends_on_tasks:
|
||||
depends_on_tasks += d.task + ","
|
||||
self.depends_on_tasks = depends_on_tasks
|
||||
|
||||
def on_update(self):
|
||||
self.update_nsm_model()
|
||||
self.check_recursion()
|
||||
self.reschedule_dependent_tasks()
|
||||
self.update_project()
|
||||
self.unassign_todo()
|
||||
self.populate_depends_on()
|
||||
def update_nsm_model(self):
|
||||
frappe.utils.nestedset.update_nsm(self)
|
||||
|
||||
def unassign_todo(self):
|
||||
if self.status == "Completed":
|
||||
close_all_assignments(self.doctype, self.name)
|
||||
if self.status == "Cancelled":
|
||||
clear(self.doctype, self.name)
|
||||
def on_update(self):
|
||||
self.update_nsm_model()
|
||||
self.check_recursion()
|
||||
self.reschedule_dependent_tasks()
|
||||
self.update_project()
|
||||
self.unassign_todo()
|
||||
self.populate_depends_on()
|
||||
|
||||
def update_total_expense_claim(self):
|
||||
self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
|
||||
where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
|
||||
def unassign_todo(self):
|
||||
if self.status == "Completed":
|
||||
close_all_assignments(self.doctype, self.name)
|
||||
if self.status == "Cancelled":
|
||||
clear(self.doctype, self.name)
|
||||
|
||||
def update_time_and_costing(self):
|
||||
tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
|
||||
sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
|
||||
sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
|
||||
,self.name, as_dict=1)[0]
|
||||
if self.status == "Open":
|
||||
self.status = "Working"
|
||||
self.total_costing_amount= tl.total_costing_amount
|
||||
self.total_billing_amount= tl.total_billing_amount
|
||||
self.actual_time= tl.time
|
||||
self.act_start_date= tl.start_date
|
||||
self.act_end_date= tl.end_date
|
||||
def update_total_expense_claim(self):
|
||||
self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
|
||||
where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
|
||||
|
||||
def update_project(self):
|
||||
if self.project and not self.flags.from_project:
|
||||
frappe.get_cached_doc("Project", self.project).update_project()
|
||||
def update_time_and_costing(self):
|
||||
tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
|
||||
sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
|
||||
sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
|
||||
,self.name, as_dict=1)[0]
|
||||
if self.status == "Open":
|
||||
self.status = "Working"
|
||||
self.total_costing_amount= tl.total_costing_amount
|
||||
self.total_billing_amount= tl.total_billing_amount
|
||||
self.actual_time= tl.time
|
||||
self.act_start_date= tl.start_date
|
||||
self.act_end_date= tl.end_date
|
||||
|
||||
def check_recursion(self):
|
||||
if self.flags.ignore_recursion_check: return
|
||||
check_list = [['task', 'parent'], ['parent', 'task']]
|
||||
for d in check_list:
|
||||
task_list, count = [self.name], 0
|
||||
while (len(task_list) > count ):
|
||||
tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
|
||||
(d[0], d[1], '%s'), cstr(task_list[count]))
|
||||
count = count + 1
|
||||
for b in tasks:
|
||||
if b[0] == self.name:
|
||||
frappe.throw(_("Circular Reference Error"), CircularReferenceError)
|
||||
if b[0]:
|
||||
task_list.append(b[0])
|
||||
def update_project(self):
|
||||
if self.project and not self.flags.from_project:
|
||||
frappe.get_cached_doc("Project", self.project).update_project()
|
||||
|
||||
if count == 15:
|
||||
break
|
||||
def check_recursion(self):
|
||||
if self.flags.ignore_recursion_check: return
|
||||
check_list = [['task', 'parent'], ['parent', 'task']]
|
||||
for d in check_list:
|
||||
task_list, count = [self.name], 0
|
||||
while (len(task_list) > count ):
|
||||
tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
|
||||
(d[0], d[1], '%s'), cstr(task_list[count]))
|
||||
count = count + 1
|
||||
for b in tasks:
|
||||
if b[0] == self.name:
|
||||
frappe.throw(_("Circular Reference Error"), CircularReferenceError)
|
||||
if b[0]:
|
||||
task_list.append(b[0])
|
||||
|
||||
def reschedule_dependent_tasks(self):
|
||||
end_date = self.exp_end_date or self.act_end_date
|
||||
if end_date:
|
||||
for task_name in frappe.db.sql("""
|
||||
select name from `tabTask` as parent
|
||||
where parent.project = %(project)s
|
||||
and parent.name in (
|
||||
select parent from `tabTask Depends On` as child
|
||||
where child.task = %(task)s and child.project = %(project)s)
|
||||
""", {'project': self.project, 'task':self.name }, as_dict=1):
|
||||
task = frappe.get_doc("Task", task_name.name)
|
||||
if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
|
||||
task_duration = date_diff(task.exp_end_date, task.exp_start_date)
|
||||
task.exp_start_date = add_days(end_date, 1)
|
||||
task.exp_end_date = add_days(task.exp_start_date, task_duration)
|
||||
task.flags.ignore_recursion_check = True
|
||||
task.save()
|
||||
if count == 15:
|
||||
break
|
||||
|
||||
def has_webform_permission(self):
|
||||
project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
|
||||
if project_user:
|
||||
return True
|
||||
def reschedule_dependent_tasks(self):
|
||||
end_date = self.exp_end_date or self.act_end_date
|
||||
if end_date:
|
||||
for task_name in frappe.db.sql("""
|
||||
select name from `tabTask` as parent
|
||||
where parent.project = %(project)s
|
||||
and parent.name in (
|
||||
select parent from `tabTask Depends On` as child
|
||||
where child.task = %(task)s and child.project = %(project)s)
|
||||
""", {'project': self.project, 'task':self.name }, as_dict=1):
|
||||
task = frappe.get_doc("Task", task_name.name)
|
||||
if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
|
||||
task_duration = date_diff(task.exp_end_date, task.exp_start_date)
|
||||
task.exp_start_date = add_days(end_date, 1)
|
||||
task.exp_end_date = add_days(task.exp_start_date, task_duration)
|
||||
task.flags.ignore_recursion_check = True
|
||||
task.save()
|
||||
|
||||
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]:
|
||||
parent.append("depends_on", {
|
||||
"doctype": "Task Depends On",
|
||||
"task": self.name,
|
||||
"subject": self.subject
|
||||
})
|
||||
parent.save()
|
||||
def has_webform_permission(self):
|
||||
project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
|
||||
if project_user:
|
||||
return True
|
||||
|
||||
def on_trash(self):
|
||||
if check_if_child_exists(self.name):
|
||||
throw(_("Child Task exists for this Task. You can not delete this Task."))
|
||||
def populate_depends_on(self):
|
||||
if self.parent_task:
|
||||
parent = frappe.get_doc('Task', self.parent_task)
|
||||
if self.name not in [row.task for row in parent.depends_on]:
|
||||
parent.append("depends_on", {
|
||||
"doctype": "Task Depends On",
|
||||
"task": self.name,
|
||||
"subject": self.subject
|
||||
})
|
||||
parent.save()
|
||||
|
||||
self.update_nsm_model()
|
||||
def on_trash(self):
|
||||
if check_if_child_exists(self.name):
|
||||
throw(_("Child Task exists for this Task. You can not delete this Task."))
|
||||
|
||||
def after_delete(self):
|
||||
self.update_project()
|
||||
self.update_nsm_model()
|
||||
|
||||
def update_status(self):
|
||||
if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
|
||||
from datetime import datetime
|
||||
if self.exp_end_date < datetime.now().date():
|
||||
self.db_set('status', 'Overdue', update_modified=False)
|
||||
self.update_project()
|
||||
def after_delete(self):
|
||||
self.update_project()
|
||||
|
||||
def update_status(self):
|
||||
if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
|
||||
from datetime import datetime
|
||||
if self.exp_end_date < datetime.now().date():
|
||||
self.db_set('status', 'Overdue', update_modified=False)
|
||||
self.update_project()
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_if_child_exists(name):
|
||||
child_tasks = frappe.get_all("Task", filters={"parent_task": name})
|
||||
child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
|
||||
return child_tasks
|
||||
child_tasks = frappe.get_all("Task", filters={"parent_task": name})
|
||||
child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
|
||||
return child_tasks
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_project(doctype, txt, searchfield, start, page_len, filters):
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
return frappe.db.sql(""" select name from `tabProject`
|
||||
where %(key)s like %(txt)s
|
||||
%(mcond)s
|
||||
order by name
|
||||
limit %(start)s, %(page_len)s""" % {
|
||||
'key': searchfield,
|
||||
'txt': frappe.db.escape('%' + txt + '%'),
|
||||
'mcond':get_match_cond(doctype),
|
||||
'start': start,
|
||||
'page_len': page_len
|
||||
})
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
return frappe.db.sql(""" select name from `tabProject`
|
||||
where %(key)s like %(txt)s
|
||||
%(mcond)s
|
||||
order by name
|
||||
limit %(start)s, %(page_len)s""" % {
|
||||
'key': searchfield,
|
||||
'txt': frappe.db.escape('%' + txt + '%'),
|
||||
'mcond':get_match_cond(doctype),
|
||||
'start': start,
|
||||
'page_len': page_len
|
||||
})
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_multiple_status(names, status):
|
||||
names = json.loads(names)
|
||||
for name in names:
|
||||
task = frappe.get_doc("Task", name)
|
||||
task.status = status
|
||||
task.save()
|
||||
names = json.loads(names)
|
||||
for name in names:
|
||||
task = frappe.get_doc("Task", name)
|
||||
task.status = status
|
||||
task.save()
|
||||
|
||||
def set_tasks_as_overdue():
|
||||
tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
|
||||
for task in tasks:
|
||||
if task.status == "Pending Review":
|
||||
if getdate(task.review_date) > getdate(today()):
|
||||
continue
|
||||
frappe.get_doc("Task", task.name).update_status()
|
||||
tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
|
||||
for task in tasks:
|
||||
if task.status == "Pending Review":
|
||||
if getdate(task.review_date) > getdate(today()):
|
||||
continue
|
||||
frappe.get_doc("Task", task.name).update_status()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
|
||||
def set_missing_values(source, target):
|
||||
target.append("time_logs", {
|
||||
"hours": source.actual_time,
|
||||
"completed": source.status == "Completed",
|
||||
"project": source.project,
|
||||
"task": source.name
|
||||
})
|
||||
def set_missing_values(source, target):
|
||||
target.append("time_logs", {
|
||||
"hours": source.actual_time,
|
||||
"completed": source.status == "Completed",
|
||||
"project": source.project,
|
||||
"task": source.name
|
||||
})
|
||||
|
||||
doclist = get_mapped_doc("Task", source_name, {
|
||||
"Task": {
|
||||
"doctype": "Timesheet"
|
||||
}
|
||||
}, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
|
||||
doclist = get_mapped_doc("Task", source_name, {
|
||||
"Task": {
|
||||
"doctype": "Timesheet"
|
||||
}
|
||||
}, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
|
||||
|
||||
return doclist
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_children(doctype, parent, task=None, project=None, is_root=False):
|
||||
|
||||
filters = [['docstatus', '<', '2']]
|
||||
filters = [['docstatus', '<', '2']]
|
||||
|
||||
if task:
|
||||
filters.append(['parent_task', '=', task])
|
||||
elif parent and not is_root:
|
||||
# via expand child
|
||||
filters.append(['parent_task', '=', parent])
|
||||
else:
|
||||
filters.append(['ifnull(`parent_task`, "")', '=', ''])
|
||||
if task:
|
||||
filters.append(['parent_task', '=', task])
|
||||
elif parent and not is_root:
|
||||
# via expand child
|
||||
filters.append(['parent_task', '=', parent])
|
||||
else:
|
||||
filters.append(['ifnull(`parent_task`, "")', '=', ''])
|
||||
|
||||
if project:
|
||||
filters.append(['project', '=', project])
|
||||
if project:
|
||||
filters.append(['project', '=', project])
|
||||
|
||||
tasks = frappe.get_list(doctype, fields=[
|
||||
'name as value',
|
||||
'subject as title',
|
||||
'is_group as expandable'
|
||||
], filters=filters, order_by='name')
|
||||
tasks = frappe.get_list(doctype, fields=[
|
||||
'name as value',
|
||||
'subject as title',
|
||||
'is_group as expandable'
|
||||
], filters=filters, order_by='name')
|
||||
|
||||
# return tasks
|
||||
return tasks
|
||||
# return tasks
|
||||
return tasks
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_node():
|
||||
from frappe.desk.treeview import make_tree_args
|
||||
args = frappe.form_dict
|
||||
args.update({
|
||||
"name_field": "subject"
|
||||
})
|
||||
args = make_tree_args(**args)
|
||||
from frappe.desk.treeview import make_tree_args
|
||||
args = frappe.form_dict
|
||||
args.update({
|
||||
"name_field": "subject"
|
||||
})
|
||||
args = make_tree_args(**args)
|
||||
|
||||
if args.parent_task == 'All Tasks' or args.parent_task == args.project:
|
||||
args.parent_task = None
|
||||
if args.parent_task == 'All Tasks' or args.parent_task == args.project:
|
||||
args.parent_task = None
|
||||
|
||||
frappe.get_doc(args).insert()
|
||||
frappe.get_doc(args).insert()
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_multiple_tasks(data, parent):
|
||||
data = json.loads(data)
|
||||
new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
|
||||
new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
|
||||
data = json.loads(data)
|
||||
new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
|
||||
new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
|
||||
|
||||
for d in data:
|
||||
if not d.get("subject"): continue
|
||||
new_doc['subject'] = d.get("subject")
|
||||
new_task = frappe.get_doc(new_doc)
|
||||
new_task.insert()
|
||||
for d in data:
|
||||
if not d.get("subject"): continue
|
||||
new_doc['subject'] = d.get("subject")
|
||||
new_task = frappe.get_doc(new_doc)
|
||||
new_task.insert()
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Task", ["lft", "rgt"])
|
||||
frappe.db.add_index("Task", ["lft", "rgt"])
|
||||
|
||||
def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date):
|
||||
if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
|
||||
frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
|
||||
if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
|
||||
frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
|
||||
|
||||
if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
|
||||
frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
|
||||
if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
|
||||
frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
|
||||
|
@ -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];
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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,56 +829,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 +1086,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):
|
||||
@ -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)
|
||||
|
||||
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"
|
||||
@ -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 > 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 22:16:53.978410",
|
||||
"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