feat(healthcare): Added Treatment Plan Template feature (#26557)

* feat: Added doctypes for treatment plan template

* feat: Added child doctype

* feat: Added validations for patient age

* chore: Clean up treatment plan template

* fix: Limit plan items to templates

* Added multiselectdialogbox for treatment plan template

* Add template name as autoname

* Clean up code

* Cleanup lint issues

* Clean up code

* Added tests for tpt filters

* Added test records

* Fix order of fields

* Added tests for care plan template

* Added age property for patient

* Clean up code

* Clean up list view

* Clean up code

* Fix lint issues

* Clean up test records

* Code cleanup

no-docs

* chore: Code cleanup

* chore: Use ORM instead of raw sql

* fix: Make treatment plan item fields mandatory

* fix: Added healthcare roles in permissions rules

* fix: Added filters for symptoms, diagnosis

* fix: Show applicable treatment plans button only if patient is present

* fix: Fix key error issues

* fix: Fix issues with filters in plan templates

* chore: Fix age filters

* refactor: appending treatment plan items

* fix: treatment plan test

Co-authored-by: Jannat Patel <31363128+pateljannat@users.noreply.github.com>
Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
This commit is contained in:
Chillar Anand 2021-08-26 22:15:30 +05:30 committed by GitHub
parent 16eed07a0f
commit f1b77360ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 567 additions and 10 deletions

View File

@ -56,6 +56,7 @@
"reqd": 1
},
{
"allow_in_quick_entry": 1,
"fieldname": "dosage_form",
"fieldtype": "Link",
"ignore_user_permissions": 1,
@ -109,7 +110,7 @@
],
"istable": 1,
"links": [],
"modified": "2020-09-30 23:32:09.495288",
"modified": "2021-06-11 11:53:06.343704",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Drug Prescription",

View File

@ -102,12 +102,19 @@ class Patient(Document):
return name
@property
def age(self):
if not self.dob:
return
dob = getdate(self.dob)
age = dateutil.relativedelta.relativedelta(getdate(), dob)
return age
def get_age(self):
age_str = ''
if self.dob:
dob = getdate(self.dob)
age = dateutil.relativedelta.relativedelta(getdate(), dob)
age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
age = self.age
if not age:
return
age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
return age_str
@frappe.whitelist()

View File

@ -185,7 +185,42 @@ frappe.ui.form.on('Patient Encounter', {
};
frm.set_value(values);
}
}
},
get_applicable_treatment_plans: function(frm) {
frappe.call({
method: 'get_applicable_treatment_plans',
doc: frm.doc,
args: {'encounter': frm.doc},
freeze: true,
freeze_message: __('Fetching Treatment Plans'),
callback: function() {
new frappe.ui.form.MultiSelectDialog({
doctype: "Treatment Plan Template",
target: this.cur_frm,
setters: {
medical_department: "",
},
action(selections) {
frappe.call({
method: 'set_treatment_plans',
doc: frm.doc,
args: selections,
}).then(() => {
frm.refresh_field('drug_prescription');
frm.refresh_field('procedure_prescription');
frm.refresh_field('lab_test_prescription');
frm.refresh_field('therapies');
});
cur_dialog.hide();
}
});
}
});
},
});
var schedule_inpatient = function(frm) {

View File

@ -31,6 +31,7 @@
"sb_symptoms",
"symptoms",
"symptoms_in_print",
"get_applicable_treatment_plans",
"physical_examination",
"diagnosis",
"diagnosis_in_print",
@ -324,11 +325,17 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "eval:doc.patient",
"fieldname": "get_applicable_treatment_plans",
"fieldtype": "Button",
"label": "Get Applicable Treatment Plans"
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-11-30 10:39:00.783119",
"modified": "2021-07-27 11:39:12.347704",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Encounter",
@ -358,4 +365,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@ -10,6 +10,7 @@ from frappe.utils import cstr, getdate, add_days
from frappe import _
from frappe.model.mapper import get_mapped_doc
class PatientEncounter(Document):
def validate(self):
self.set_title()
@ -33,6 +34,85 @@ class PatientEncounter(Document):
self.title = _('{0} with {1}').format(self.patient_name or self.patient,
self.practitioner_name or self.practitioner)[:100]
@frappe.whitelist()
@staticmethod
def get_applicable_treatment_plans(encounter):
patient = frappe.get_doc('Patient', encounter['patient'])
plan_filters = {}
plan_filters['name'] = ['in', []]
age = patient.age
if age:
plan_filters['patient_age_from'] = ['<=', age.years]
plan_filters['patient_age_to'] = ['>=', age.years]
gender = patient.sex
if gender:
plan_filters['gender'] = ['in', [gender, None]]
diagnosis = encounter.get('diagnosis')
if diagnosis:
diagnosis = [_diagnosis['diagnosis'] for _diagnosis in encounter['diagnosis']]
filters = [
['diagnosis', 'in', diagnosis],
['parenttype', '=', 'Treatment Plan Template'],
]
diagnosis = frappe.get_list('Patient Encounter Diagnosis', filters=filters, fields='*')
plan_names = [_diagnosis['parent'] for _diagnosis in diagnosis]
plan_filters['name'][1].extend(plan_names)
symptoms = encounter.get('symptoms')
if symptoms:
symptoms = [symptom['complaint'] for symptom in encounter['symptoms']]
filters = [
['complaint', 'in', symptoms],
['parenttype', '=', 'Treatment Plan Template'],
]
symptoms = frappe.get_list('Patient Encounter Symptom', filters=filters, fields='*')
plan_names = [symptom['parent'] for symptom in symptoms]
plan_filters['name'][1].extend(plan_names)
if not plan_filters['name'][1]:
plan_filters.pop('name')
plans = frappe.get_list('Treatment Plan Template', fields='*', filters=plan_filters)
return plans
@frappe.whitelist()
def set_treatment_plans(self, treatment_plans=None):
for treatment_plan in treatment_plans:
self.set_treatment_plan(treatment_plan)
def set_treatment_plan(self, plan):
plan_items = frappe.get_list('Treatment Plan Template Item', filters={'parent': plan}, fields='*')
for plan_item in plan_items:
self.set_treatment_plan_item(plan_item)
drugs = frappe.get_list('Drug Prescription', filters={'parent': plan}, fields='*')
for drug in drugs:
self.append('drug_prescription', drug)
self.save()
def set_treatment_plan_item(self, plan_item):
if plan_item.type == 'Clinical Procedure Template':
self.append('procedure_prescription', {
'procedure': plan_item.template
})
if plan_item.type == 'Lab Test Template':
self.append('lab_test_prescription', {
'lab_test_code': plan_item.template
})
if plan_item.type == 'Therapy Type':
self.append('therapies', {
'therapy_type': plan_item.template
})
@frappe.whitelist()
def make_ip_medication_order(source_name, target_doc=None):
def set_missing_values(source, target):

View File

@ -4,5 +4,82 @@
from __future__ import unicode_literals
import unittest
import frappe
from erpnext.healthcare.doctype.patient_encounter.patient_encounter import PatientEncounter
class TestPatientEncounter(unittest.TestCase):
pass
def setUp(self):
try:
gender_m = frappe.get_doc({
'doctype': 'Gender',
'gender': 'MALE'
}).insert()
gender_f = frappe.get_doc({
'doctype': 'Gender',
'gender': 'FEMALE'
}).insert()
except frappe.exceptions.DuplicateEntryError:
gender_m = frappe.get_doc({
'doctype': 'Gender',
'gender': 'MALE'
})
gender_f = frappe.get_doc({
'doctype': 'Gender',
'gender': 'FEMALE'
})
self.patient_male = frappe.get_doc({
'doctype': 'Patient',
'first_name': 'John',
'sex': gender_m.gender,
}).insert()
self.patient_female = frappe.get_doc({
'doctype': 'Patient',
'first_name': 'Curie',
'sex': gender_f.gender,
}).insert()
self.practitioner = frappe.get_doc({
'doctype': 'Healthcare Practitioner',
'first_name': 'Doc',
'sex': 'MALE',
}).insert()
try:
self.care_plan_male = frappe.get_doc({
'doctype': 'Treatment Plan Template',
'template_name': 'test plan - m',
'gender': gender_m.gender,
}).insert()
self.care_plan_female = frappe.get_doc({
'doctype': 'Treatment Plan Template',
'template_name': 'test plan - f',
'gender': gender_f.gender,
}).insert()
except frappe.exceptions.DuplicateEntryError:
self.care_plan_male = frappe.get_doc({
'doctype': 'Treatment Plan Template',
'template_name': 'test plan - m',
'gender': gender_m.gender,
})
self.care_plan_female = frappe.get_doc({
'doctype': 'Treatment Plan Template',
'template_name': 'test plan - f',
'gender': gender_f.gender,
})
def test_treatment_plan_template_filter(self):
encounter = frappe.get_doc({
'doctype': 'Patient Encounter',
'patient': self.patient_male.name,
'practitioner': self.practitioner.name,
}).insert()
plans = PatientEncounter.get_applicable_treatment_plans(encounter.as_dict())
self.assertEqual(plans[0]['name'], self.care_plan_male.template_name)
encounter = frappe.get_doc({
'doctype': 'Patient Encounter',
'patient': self.patient_female.name,
'practitioner': self.practitioner.name,
}).insert()
plans = PatientEncounter.get_applicable_treatment_plans(encounter.as_dict())
self.assertEqual(plans[0]['name'], self.care_plan_female.template_name)

View File

@ -0,0 +1,7 @@
[
{
"doctype": "Treatment Plan Template",
"template_name": "Chemo",
"patient_age_from": 21
}
]

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestTreatmentPlanTemplate(unittest.TestCase):
pass

View File

@ -0,0 +1,14 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Treatment Plan Template', {
refresh: function (frm) {
frm.set_query('type', 'items', function () {
return {
filters: {
'name': ['in', ['Lab Test Template', 'Clinical Procedure Template', 'Therapy Type']],
}
};
});
},
});

View File

@ -0,0 +1,189 @@
{
"actions": [],
"autoname": "field:template_name",
"creation": "2021-06-10 10:14:17.901273",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_1",
"template_name",
"description",
"practitioners",
"disabled",
"column_break_1",
"medical_department",
"goal",
"order_group",
"section_break_8",
"patient_age_from",
"complaints",
"gender",
"column_break_12",
"patient_age_to",
"diagnosis",
"plan_items_section",
"items",
"drugs"
],
"fields": [
{
"fieldname": "section_break_1",
"fieldtype": "Section Break",
"label": "Plan Details"
},
{
"fieldname": "medical_department",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Medical Department",
"options": "Medical Department"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "goal",
"fieldtype": "Small Text",
"label": "Goal"
},
{
"fieldname": "practitioners",
"fieldtype": "Table MultiSelect",
"label": "Practitioners",
"options": "Treatment Plan Template Practitioner"
},
{
"fieldname": "order_group",
"fieldtype": "Link",
"label": "Order Group",
"options": "Patient Encounter",
"read_only": 1
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"label": "Plan Conditions"
},
{
"fieldname": "template_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Template Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "patient_age_from",
"fieldtype": "Int",
"label": "Patient Age From",
"non_negative": 1
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"fieldname": "patient_age_to",
"fieldtype": "Int",
"label": "Patient Age To",
"non_negative": 1
},
{
"fieldname": "gender",
"fieldtype": "Link",
"label": "Gender",
"options": "Gender"
},
{
"fieldname": "complaints",
"fieldtype": "Table MultiSelect",
"label": "Complaints",
"options": "Patient Encounter Symptom"
},
{
"fieldname": "diagnosis",
"fieldtype": "Table MultiSelect",
"label": "Diagnosis",
"options": "Patient Encounter Diagnosis"
},
{
"fieldname": "plan_items_section",
"fieldtype": "Section Break",
"label": "Plan Items"
},
{
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"options": "Treatment Plan Template Item"
},
{
"fieldname": "drugs",
"fieldtype": "Table",
"label": "Drugs",
"options": "Drug Prescription"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-08-18 02:41:58.354296",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Treatment Plan Template",
"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": "Physician",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Healthcare Administrator",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "template_name",
"track_changes": 1
}

View File

@ -0,0 +1,19 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class TreatmentPlanTemplate(Document):
def validate(self):
self.validate_age()
def validate_age(self):
if self.patient_age_from and self.patient_age_from < 0:
frappe.throw(_('Patient Age From cannot be less than 0'))
if self.patient_age_to and self.patient_age_to < 0:
frappe.throw(_('Patient Age To cannot be less than 0'))
if self.patient_age_to and self.patient_age_from and \
self.patient_age_to < self.patient_age_from:
frappe.throw(_('Patient Age To cannot be less than Patient Age From'))

View File

@ -0,0 +1,10 @@
frappe.listview_settings['Treatment Plan Template'] = {
get_indicator: function(doc) {
var colors = {
1: 'gray',
0: 'blue',
};
let label = doc.disabled == 1 ? 'Disabled' : 'Enabled';
return [__(label), colors[doc.disabled], 'disable,=,' + doc.disabled];
}
};

View File

@ -0,0 +1,55 @@
{
"actions": [],
"creation": "2021-06-10 11:47:29.194795",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"type",
"template",
"qty",
"instructions"
],
"fields": [
{
"fieldname": "type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Type",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "template",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Template",
"options": "type",
"reqd": 1
},
{
"default": "1",
"fieldname": "qty",
"fieldtype": "Int",
"label": "Qty"
},
{
"fieldname": "instructions",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Instructions"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-17 11:19:03.515441",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Treatment Plan Template Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class TreatmentPlanTemplateItem(Document):
pass

View File

@ -0,0 +1,32 @@
{
"actions": [],
"creation": "2021-06-10 10:37:56.669416",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"practitioner"
],
"fields": [
{
"fieldname": "practitioner",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Practitioner",
"options": "Healthcare Practitioner",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-11 16:05:06.733299",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Treatment Plan Template Practitioner",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class TreatmentPlanTemplatePractitioner(Document):
pass