feat(Healthcare): Capacity for Service Unit, concurrent appointments based on capacity, Patient Appointments (#27219)

* feat(Healthcare): Capacity for Service Unit, concurrent appointments based on Capacity, Patient enhancements

* fix: appointment test

Co-authored-by: Anoop <3326959+akurungadam@users.noreply.github.com>
This commit is contained in:
Rucha Mahabal 2021-08-30 13:10:18 +05:30 committed by GitHub
parent e5e00700e5
commit 212eb4bc1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1027 additions and 318 deletions

View File

@ -11,7 +11,7 @@ test_dependencies = ['Item']
class TestClinicalProcedure(unittest.TestCase):
def test_procedure_template_item(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
procedure_template = create_clinical_procedure_template()
self.assertTrue(frappe.db.exists('Item', procedure_template.item))
@ -20,7 +20,7 @@ class TestClinicalProcedure(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Item', procedure_template.item, 'disabled'), 1)
def test_consumables(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
procedure_template = create_clinical_procedure_template()
procedure_template.allow_stock_consumption = 1
consumable = create_consumable()

View File

@ -27,7 +27,7 @@ class TestFeeValidity(unittest.TestCase):
healthcare_settings.automate_appointment_invoicing = 1
healthcare_settings.op_consulting_charge_item = item
healthcare_settings.save(ignore_permissions=True)
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
# For first appointment, invoice is generated. First appointment not considered in fee validity
appointment = create_appointment(patient, practitioner, nowdate())

View File

@ -7,8 +7,8 @@ frappe.ui.form.on('Healthcare Service Unit', {
// get query select healthcare service unit
frm.fields_dict['parent_healthcare_service_unit'].get_query = function(doc) {
return{
filters:[
return {
filters: [
['Healthcare Service Unit', 'is_group', '=', 1],
['Healthcare Service Unit', 'name', '!=', doc.healthcare_service_unit_name]
]
@ -21,6 +21,14 @@ frappe.ui.form.on('Healthcare Service Unit', {
frm.add_custom_button(__('Healthcare Service Unit Tree'), function() {
frappe.set_route('Tree', 'Healthcare Service Unit');
});
frm.set_query('warehouse', function() {
return {
filters: {
'company': frm.doc.company
}
};
});
},
set_root_readonly: function(frm) {
// read-only for root healthcare service unit
@ -43,5 +51,10 @@ frappe.ui.form.on('Healthcare Service Unit', {
else {
frm.set_df_property('service_unit_type', 'reqd', 1);
}
},
overlap_appointments: function(frm) {
if (frm.doc.overlap_appointments == 0) {
frm.set_value('service_unit_capacity', '');
}
}
});

View File

@ -16,6 +16,7 @@
"service_unit_type",
"allow_appointments",
"overlap_appointments",
"service_unit_capacity",
"inpatient_occupancy",
"occupancy_status",
"column_break_9",
@ -31,6 +32,8 @@
{
"fieldname": "healthcare_service_unit_name",
"fieldtype": "Data",
"hide_days": 1,
"hide_seconds": 1,
"in_global_search": 1,
"in_list_view": 1,
"label": "Service Unit",
@ -41,6 +44,8 @@
"bold": 1,
"fieldname": "parent_healthcare_service_unit",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Parent Service Unit",
@ -52,6 +57,8 @@
"depends_on": "eval:doc.inpatient_occupancy != 1 && doc.allow_appointments != 1",
"fieldname": "is_group",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Is Group"
},
{
@ -59,6 +66,8 @@
"depends_on": "eval:doc.is_group != 1",
"fieldname": "service_unit_type",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Service Unit Type",
"options": "Healthcare Service Unit Type"
},
@ -68,6 +77,8 @@
"fetch_from": "service_unit_type.allow_appointments",
"fieldname": "allow_appointments",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"label": "Allow Appointments",
"no_copy": 1,
@ -79,6 +90,8 @@
"fetch_from": "service_unit_type.overlap_appointments",
"fieldname": "overlap_appointments",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Allow Overlap",
"no_copy": 1,
"read_only": 1
@ -90,6 +103,8 @@
"fetch_from": "service_unit_type.inpatient_occupancy",
"fieldname": "inpatient_occupancy",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"label": "Inpatient Occupancy",
"no_copy": 1,
@ -100,6 +115,8 @@
"depends_on": "eval:doc.inpatient_occupancy == 1",
"fieldname": "occupancy_status",
"fieldtype": "Select",
"hide_days": 1,
"hide_seconds": 1,
"label": "Occupancy Status",
"no_copy": 1,
"options": "Vacant\nOccupied",
@ -107,13 +124,17 @@
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1
},
{
"bold": 1,
"depends_on": "eval:doc.is_group != 1",
"fieldname": "warehouse",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Warehouse",
"no_copy": 1,
"options": "Warehouse"
@ -121,6 +142,8 @@
{
"fieldname": "company",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"ignore_user_permissions": 1,
"in_list_view": 1,
"in_standard_filter": 1,
@ -134,6 +157,8 @@
"fieldname": "lft",
"fieldtype": "Int",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "lft",
"no_copy": 1,
"print_hide": 1,
@ -143,6 +168,8 @@
"fieldname": "rgt",
"fieldtype": "Int",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "rgt",
"no_copy": 1,
"print_hide": 1,
@ -152,6 +179,8 @@
"fieldname": "old_parent",
"fieldtype": "Link",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"ignore_user_permissions": 1,
"label": "Old Parent",
"no_copy": 1,
@ -163,14 +192,26 @@
"collapsible": 1,
"fieldname": "tree_details_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Tree Details"
},
{
"depends_on": "eval:doc.overlap_appointments == 1",
"fieldname": "service_unit_capacity",
"fieldtype": "Int",
"label": "Service Unit Capacity",
"mandatory_depends_on": "eval:doc.overlap_appointments == 1",
"non_negative": 1
}
],
"is_tree": 1,
"links": [],
"modified": "2020-05-20 18:26:56.065543",
"modified": "2021-08-19 14:09:11.643464",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Service Unit",
"nsm_parent_field": "parent_healthcare_service_unit",
"owner": "Administrator",
"permissions": [
{

View File

@ -5,14 +5,21 @@
from __future__ import unicode_literals
from frappe.utils.nestedset import NestedSet
from frappe.utils import cint, cstr
import frappe
from frappe import _
import json
class HealthcareServiceUnit(NestedSet):
nsm_parent_field = 'parent_healthcare_service_unit'
def validate(self):
self.set_service_unit_properties()
def autoname(self):
if self.company:
suffix = " - " + frappe.get_cached_value('Company', self.company, "abbr")
suffix = " - " + frappe.get_cached_value('Company', self.company, 'abbr')
if not self.healthcare_service_unit_name.endswith(suffix):
self.name = self.healthcare_service_unit_name + suffix
else:
@ -22,16 +29,86 @@ class HealthcareServiceUnit(NestedSet):
super(HealthcareServiceUnit, self).on_update()
self.validate_one_root()
def after_insert(self):
def set_service_unit_properties(self):
if self.is_group:
self.allow_appointments = 0
self.overlap_appointments = 0
self.inpatient_occupancy = 0
elif self.service_unit_type:
self.allow_appointments = False
self.overlap_appointments = False
self.inpatient_occupancy = False
self.service_unit_capacity = 0
self.occupancy_status = ''
self.service_unit_type = ''
elif self.service_unit_type != '':
service_unit_type = frappe.get_doc('Healthcare Service Unit Type', self.service_unit_type)
self.allow_appointments = service_unit_type.allow_appointments
self.overlap_appointments = service_unit_type.overlap_appointments
self.inpatient_occupancy = service_unit_type.inpatient_occupancy
if self.inpatient_occupancy:
if self.inpatient_occupancy and self.occupancy_status != '':
self.occupancy_status = 'Vacant'
self.overlap_appointments = 0
if service_unit_type.overlap_appointments:
self.overlap_appointments = True
else:
self.overlap_appointments = False
self.service_unit_capacity = 0
if self.overlap_appointments:
if not self.service_unit_capacity:
frappe.throw(_('Please set a valid Service Unit Capacity to enable Overlapping Appointments'),
title=_('Mandatory'))
@frappe.whitelist()
def add_multiple_service_units(parent, data):
'''
parent - parent service unit under which the service units are to be created
data (dict) - company, healthcare_service_unit_name, count, service_unit_type, warehouse, service_unit_capacity
'''
if not parent or not data:
return
data = json.loads(data)
company = data.get('company') or \
frappe.defaults.get_defaults().get('company') or \
frappe.db.get_single_value('Global Defaults', 'default_company')
if not data.get('healthcare_service_unit_name') or not company:
frappe.throw(_('Service Unit Name and Company are mandatory to create Healthcare Service Units'),
title=_('Missing Required Fields'))
count = cint(data.get('count') or 0)
if count <= 0:
frappe.throw(_('Number of Service Units to be created should at least be 1'),
title=_('Invalid Number of Service Units'))
capacity = cint(data.get('service_unit_capacity') or 1)
service_unit = {
'doctype': 'Healthcare Service Unit',
'parent_healthcare_service_unit': parent,
'service_unit_type': data.get('service_unit_type') or None,
'service_unit_capacity': capacity if capacity > 0 else 1,
'warehouse': data.get('warehouse') or None,
'company': company
}
service_unit_name = '{}'.format(data.get('healthcare_service_unit_name').strip(' -'))
last_suffix = frappe.db.sql("""SELECT
IFNULL(MAX(CAST(SUBSTRING(name FROM %(start)s FOR 4) AS UNSIGNED)), 0)
FROM `tabHealthcare Service Unit`
WHERE name like %(prefix)s AND company=%(company)s""",
{'start': len(service_unit_name)+2, 'prefix': '{}-%'.format(service_unit_name), 'company': company},
as_list=1)[0][0]
start_suffix = cint(last_suffix) + 1
failed_list = []
for i in range(start_suffix, count + start_suffix):
# name to be in the form WARD-####
service_unit['healthcare_service_unit_name'] = '{}-{}'.format(service_unit_name, cstr('%0*d' % (4, i)))
service_unit_doc = frappe.get_doc(service_unit)
try:
service_unit_doc.insert()
except Exception:
failed_list.append(service_unit['healthcare_service_unit_name'])
return failed_list

View File

@ -1,35 +1,185 @@
frappe.treeview_settings["Healthcare Service Unit"] = {
breadcrumbs: "Healthcare Service Unit",
title: __("Healthcare Service Unit"),
frappe.provide("frappe.treeview_settings");
frappe.treeview_settings['Healthcare Service Unit'] = {
breadcrumbs: 'Healthcare Service Unit',
title: __('Service Unit Tree'),
get_tree_root: false,
filters: [{
fieldname: "company",
fieldtype: "Select",
options: erpnext.utils.get_tree_options("company"),
label: __("Company"),
default: erpnext.utils.get_tree_default("company")
}],
get_tree_nodes: 'erpnext.healthcare.utils.get_children',
ignore_fields:["parent_healthcare_service_unit"],
onrender: function(node) {
if (node.data.occupied_out_of_vacant!==undefined) {
$('<span class="balance-area pull-right">'
+ " " + node.data.occupied_out_of_vacant
filters: [{
fieldname: 'company',
fieldtype: 'Select',
options: erpnext.utils.get_tree_options('company'),
label: __('Company'),
default: erpnext.utils.get_tree_default('company')
}],
fields: [
{
fieldtype: 'Data', fieldname: 'healthcare_service_unit_name', label: __('New Service Unit Name'),
reqd: true
},
{
fieldtype: 'Check', fieldname: 'is_group', label: __('Is Group'),
description: __("Child nodes can be only created under 'Group' type nodes")
},
{
fieldtype: 'Link', fieldname: 'service_unit_type', label: __('Service Unit Type'),
options: 'Healthcare Service Unit Type', description: __('Type of the new Service Unit'),
depends_on: 'eval:!doc.is_group', default: '',
onchange: () => {
if (cur_dialog) {
if (cur_dialog.fields_dict.service_unit_type.value) {
frappe.db.get_value('Healthcare Service Unit Type',
cur_dialog.fields_dict.service_unit_type.value, 'overlap_appointments')
.then(r => {
if (r.message.overlap_appointments) {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', false);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', true);
} else {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', true);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', false);
}
});
} else {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', true);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', false);
}
}
}
},
{
fieldtype: 'Int', fieldname: 'service_unit_capacity', label: __('Service Unit Capacity'),
description: __('Sets the number of concurrent appointments allowed'), reqd: false,
depends_on: "eval:!doc.is_group && doc.service_unit_type != ''", hidden: true
},
{
fieldtype: 'Link', fieldname: 'warehouse', label: __('Warehouse'), options: 'Warehouse',
description: __('Optional, if you want to manage stock separately for this Service Unit'),
depends_on: 'eval:!doc.is_group'
},
{
fieldtype: 'Link', fieldname: 'company', label: __('Company'), options: 'Company', reqd: true,
default: () => {
return cur_page.page.page.fields_dict.company.value;
}
}
],
ignore_fields: ['parent_healthcare_service_unit'],
onrender: function (node) {
if (node.data.occupied_of_available !== undefined) {
$("<span class='balance-area pull-right text-muted small'>"
+ ' ' + node.data.occupied_of_available
+ '</span>').insertBefore(node.$ul);
}
if (node.data && node.data.inpatient_occupancy!==undefined) {
if (node.data && node.data.inpatient_occupancy !== undefined) {
if (node.data.inpatient_occupancy == 1) {
if (node.data.occupancy_status == "Occupied") {
$('<span class="balance-area pull-right">'
+ " " + node.data.occupancy_status
if (node.data.occupancy_status == 'Occupied') {
$("<span class='balance-area pull-right small'>"
+ ' ' + node.data.occupancy_status
+ '</span>').insertBefore(node.$ul);
}
if (node.data.occupancy_status == "Vacant") {
$('<span class="balance-area pull-right">'
+ " " + node.data.occupancy_status
if (node.data.occupancy_status == 'Vacant') {
$("<span class='balance-area pull-right text-muted small'>"
+ ' ' + node.data.occupancy_status
+ '</span>').insertBefore(node.$ul);
}
}
}
},
post_render: function (treeview) {
frappe.treeview_settings['Healthcare Service Unit'].treeview = {};
$.extend(frappe.treeview_settings['Healthcare Service Unit'].treeview, treeview);
},
toolbar: [
{
label: __('Add Multiple'),
condition: function (node) {
return node.expandable;
},
click: function (node) {
const dialog = new frappe.ui.Dialog({
title: __('Add Multiple Service Units'),
fields: [
{
fieldtype: 'Data', fieldname: 'healthcare_service_unit_name', label: __('Service Unit Name'),
reqd: true, description: __("Will be serially suffixed to maintain uniquness. Example: 'Ward' will be named as 'Ward-####'"),
},
{
fieldtype: 'Int', fieldname: 'count', label: __('Number of Service Units'),
reqd: true
},
{
fieldtype: 'Link', fieldname: 'service_unit_type', label: __('Service Unit Type'),
options: 'Healthcare Service Unit Type', description: __('Type of the new Service Unit'),
depends_on: 'eval:!doc.is_group', default: '', reqd: true,
onchange: () => {
if (cur_dialog) {
if (cur_dialog.fields_dict.service_unit_type.value) {
frappe.db.get_value('Healthcare Service Unit Type',
cur_dialog.fields_dict.service_unit_type.value, 'overlap_appointments')
.then(r => {
if (r.message.overlap_appointments) {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', false);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', true);
} else {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', true);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', false);
}
});
} else {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', true);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', false);
}
}
}
},
{
fieldtype: 'Int', fieldname: 'service_unit_capacity', label: __('Service Unit Capacity'),
description: __('Sets the number of concurrent appointments allowed'), reqd: false,
depends_on: "eval:!doc.is_group && doc.service_unit_type != ''", hidden: true
},
{
fieldtype: 'Link', fieldname: 'warehouse', label: __('Warehouse'), options: 'Warehouse',
description: __('Optional, if you want to manage stock separately for this Service Unit'),
},
{
fieldtype: 'Link', fieldname: 'company', label: __('Company'), options: 'Company', reqd: true,
default: () => {
return cur_page.page.page.fields_dict.company.get_value();
}
}
],
primary_action: () => {
dialog.hide();
let vals = dialog.get_values();
if (!vals) return;
return frappe.call({
method: 'erpnext.healthcare.doctype.healthcare_service_unit.healthcare_service_unit.add_multiple_service_units',
args: {
parent: node.data.value,
data: vals
},
callback: function (r) {
if (!r.exc && r.message) {
frappe.treeview_settings['Healthcare Service Unit'].treeview.tree.load_children(node, true);
frappe.show_alert({
message: __('{0} Service Units created', [vals.count - r.message.length]),
indicator: 'green'
});
} else {
frappe.msgprint(__('Could not create Service Units'));
}
},
freeze: true,
freeze_message: __('Creating {0} Service Units', [vals.count])
});
},
primary_action_label: __('Create')
});
dialog.show();
}
}
],
extend_toolbar: true
};

View File

@ -68,8 +68,8 @@ let change_item_code = function(frm, doc) {
if (values) {
frappe.call({
"method": "erpnext.healthcare.doctype.healthcare_service_unit_type.healthcare_service_unit_type.change_item_code",
"args": {item: doc.item, item_code: values['item_code'], doc_name: doc.name},
callback: function () {
"args": { item: doc.item, item_code: values['item_code'], doc_name: doc.name },
callback: function() {
frm.reload_doc();
}
});

View File

@ -29,6 +29,8 @@
{
"fieldname": "service_unit_type",
"fieldtype": "Data",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"label": "Service Unit Type",
"no_copy": 1,
@ -41,6 +43,8 @@
"depends_on": "eval:doc.inpatient_occupancy != 1",
"fieldname": "allow_appointments",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Allow Appointments"
},
{
@ -49,6 +53,8 @@
"depends_on": "eval:doc.allow_appointments == 1 && doc.inpatient_occupany != 1",
"fieldname": "overlap_appointments",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Allow Overlap"
},
{
@ -57,6 +63,8 @@
"depends_on": "eval:doc.allow_appointments != 1",
"fieldname": "inpatient_occupancy",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Inpatient Occupancy"
},
{
@ -65,17 +73,23 @@
"depends_on": "eval:doc.inpatient_occupancy == 1 && doc.allow_appointments != 1",
"fieldname": "is_billable",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Is Billable"
},
{
"depends_on": "is_billable",
"fieldname": "item_details",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Item Details"
},
{
"fieldname": "item",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Item",
"no_copy": 1,
"options": "Item",
@ -84,6 +98,8 @@
{
"fieldname": "item_code",
"fieldtype": "Data",
"hide_days": 1,
"hide_seconds": 1,
"label": "Item Code",
"mandatory_depends_on": "eval: doc.is_billable == 1",
"no_copy": 1
@ -91,6 +107,8 @@
{
"fieldname": "item_group",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Item Group",
"mandatory_depends_on": "eval: doc.is_billable == 1",
"options": "Item Group"
@ -98,6 +116,8 @@
{
"fieldname": "uom",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "UOM",
"mandatory_depends_on": "eval: doc.is_billable == 1",
"options": "UOM"
@ -105,28 +125,38 @@
{
"fieldname": "no_of_hours",
"fieldtype": "Int",
"hide_days": 1,
"hide_seconds": 1,
"label": "UOM Conversion in Hours",
"mandatory_depends_on": "eval: doc.is_billable == 1"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1
},
{
"fieldname": "rate",
"fieldtype": "Currency",
"hide_days": 1,
"hide_seconds": 1,
"label": "Rate / UOM"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Disabled",
"no_copy": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "Description"
},
{
@ -134,11 +164,13 @@
"fieldname": "change_in_item",
"fieldtype": "Check",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "Change in Item"
}
],
"links": [],
"modified": "2020-05-20 15:31:09.627516",
"modified": "2021-08-19 17:52:30.266667",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Service Unit Type",

View File

@ -151,7 +151,7 @@ def get_healthcare_service_unit(unit_name=None):
if not service_unit:
service_unit = frappe.new_doc("Healthcare Service Unit")
service_unit.healthcare_service_unit_name = unit_name or "Test Service Unit Ip Occupancy"
service_unit.healthcare_service_unit_name = unit_name or "_Test Service Unit Ip Occupancy"
service_unit.company = "_Test Company"
service_unit.service_unit_type = get_service_unit_type()
service_unit.inpatient_occupancy = 1
@ -159,12 +159,12 @@ def get_healthcare_service_unit(unit_name=None):
service_unit.is_group = 0
service_unit_parent_name = frappe.db.exists({
"doctype": "Healthcare Service Unit",
"healthcare_service_unit_name": "All Healthcare Service Units",
"healthcare_service_unit_name": "_Test All Healthcare Service Units",
"is_group": 1
})
if not service_unit_parent_name:
parent_service_unit = frappe.new_doc("Healthcare Service Unit")
parent_service_unit.healthcare_service_unit_name = "All Healthcare Service Units"
parent_service_unit.healthcare_service_unit_name = "_Test All Healthcare Service Units"
parent_service_unit.is_group = 1
parent_service_unit.save(ignore_permissions = True)
service_unit.parent_healthcare_service_unit = parent_service_unit.name
@ -180,7 +180,7 @@ def get_service_unit_type():
if not service_unit_type:
service_unit_type = frappe.new_doc("Healthcare Service Unit Type")
service_unit_type.service_unit_type = "Test Service Unit Type Ip Occupancy"
service_unit_type.service_unit_type = "_Test Service Unit Type Ip Occupancy"
service_unit_type.inpatient_occupancy = 1
service_unit_type.save(ignore_permissions = True)
return service_unit_type.name

View File

@ -26,31 +26,39 @@ frappe.ui.form.on('Patient', {
}
if (frm.doc.patient_name && frappe.user.has_role('Physician')) {
frm.add_custom_button(__('Patient Progress'), function() {
frappe.route_options = {'patient': frm.doc.name};
frappe.set_route('patient-progress');
}, __('View'));
frm.add_custom_button(__('Patient History'), function() {
frappe.route_options = {'patient': frm.doc.name};
frappe.set_route('patient_history');
},'View');
}, __('View'));
}
if (!frm.doc.__islocal && (frappe.user.has_role('Nursing User') || frappe.user.has_role('Physician'))) {
frm.add_custom_button(__('Vital Signs'), function () {
create_vital_signs(frm);
}, 'Create');
frm.add_custom_button(__('Medical Record'), function () {
create_medical_record(frm);
}, 'Create');
frm.add_custom_button(__('Patient Encounter'), function () {
create_encounter(frm);
}, 'Create');
frm.toggle_enable(['customer'], 0); // ToDo, allow change only if no transactions booked or better, add merge option
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Patient'};
frm.toggle_display(['address_html', 'contact_html'], !frm.is_new());
if (!frm.is_new()) {
if ((frappe.user.has_role('Nursing User') || frappe.user.has_role('Physician'))) {
frm.add_custom_button(__('Medical Record'), function () {
create_medical_record(frm);
}, 'Create');
frm.toggle_enable(['customer'], 0);
}
frappe.contacts.render_address_and_contact(frm);
erpnext.utils.set_party_dashboard_indicators(frm);
} else {
frappe.contacts.clear_address_and_contact(frm);
}
},
onload: function (frm) {
if (!frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html('');
}
if (frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(frm.doc.dob)}`);
} else {
$(frm.fields_dict['age_html'].wrapper).html('');
}
}
});
@ -59,16 +67,14 @@ frappe.ui.form.on('Patient', 'dob', function(frm) {
if (frm.doc.dob) {
let today = new Date();
let birthDate = new Date(frm.doc.dob);
if (today < birthDate){
if (today < birthDate) {
frappe.msgprint(__('Please select a valid Date'));
frappe.model.set_value(frm.doctype,frm.docname, 'dob', '');
}
else {
} else {
let age_str = get_age(frm.doc.dob);
$(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${age_str}`);
}
}
else {
} else {
$(frm.fields_dict['age_html'].wrapper).html('');
}
});

View File

@ -1,6 +1,6 @@
{
"actions": [],
"allow_copy": 1,
"allow_events_in_timeline": 1,
"allow_import": 1,
"allow_rename": 1,
"autoname": "naming_series:",
@ -24,12 +24,19 @@
"image",
"column_break_14",
"status",
"uid",
"inpatient_record",
"inpatient_status",
"report_preference",
"mobile",
"email",
"phone",
"email",
"invite_user",
"user_id",
"address_contacts",
"address_html",
"column_break_22",
"contact_html",
"customer_details_section",
"customer",
"customer_group",
@ -74,6 +81,7 @@
"fieldtype": "Select",
"in_preview": 1,
"label": "Inpatient Status",
"no_copy": 1,
"options": "\nAdmission Scheduled\nAdmitted\nDischarge Scheduled",
"read_only": 1
},
@ -81,6 +89,7 @@
"fieldname": "inpatient_record",
"fieldtype": "Link",
"label": "Inpatient Record",
"no_copy": 1,
"options": "Inpatient Record",
"read_only": 1
},
@ -101,6 +110,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Full Name",
"no_copy": 1,
"read_only": 1,
"search_index": 1
},
@ -118,6 +128,7 @@
"fieldtype": "Select",
"in_preview": 1,
"label": "Blood Group",
"no_copy": 1,
"options": "\nA Positive\nA Negative\nAB Positive\nAB Negative\nB Positive\nB Negative\nO Positive\nO Negative"
},
{
@ -125,7 +136,8 @@
"fieldname": "dob",
"fieldtype": "Date",
"in_preview": 1,
"label": "Date of birth"
"label": "Date of birth",
"no_copy": 1
},
{
"fieldname": "age_html",
@ -167,6 +179,7 @@
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Customer",
"no_copy": 1,
"options": "Customer",
"set_only_once": 1
},
@ -183,6 +196,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Mobile",
"no_copy": 1,
"options": "Phone"
},
{
@ -192,6 +206,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Email",
"no_copy": 1,
"options": "Email"
},
{
@ -199,6 +214,7 @@
"fieldtype": "Data",
"in_filter": 1,
"label": "Phone",
"no_copy": 1,
"options": "Phone"
},
{
@ -230,7 +246,8 @@
"fieldname": "medication",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Medication"
"label": "Medication",
"no_copy": 1
},
{
"fieldname": "column_break_20",
@ -240,13 +257,15 @@
"fieldname": "medical_history",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Medical History"
"label": "Medical History",
"no_copy": 1
},
{
"fieldname": "surgical_history",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Surgical History"
"label": "Surgical History",
"no_copy": 1
},
{
"collapsible": 1,
@ -258,8 +277,8 @@
"fieldname": "occupation",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"in_standard_filter": 1,
"label": "Occupation"
"label": "Occupation",
"no_copy": 1
},
{
"fieldname": "column_break_25",
@ -269,6 +288,7 @@
"fieldname": "marital_status",
"fieldtype": "Select",
"label": "Marital Status",
"no_copy": 1,
"options": "\nSingle\nMarried\nDivorced\nWidow"
},
{
@ -281,25 +301,29 @@
"fieldname": "tobacco_past_use",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"label": "Tobacco Consumption (Past)"
"label": "Tobacco Consumption (Past)",
"no_copy": 1
},
{
"fieldname": "tobacco_current_use",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"label": "Tobacco Consumption (Present)"
"label": "Tobacco Consumption (Present)",
"no_copy": 1
},
{
"fieldname": "alcohol_past_use",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"label": "Alcohol Consumption (Past)"
"label": "Alcohol Consumption (Past)",
"no_copy": 1
},
{
"fieldname": "alcohol_current_use",
"fieldtype": "Data",
"ignore_user_permissions": 1,
"label": "Alcohol Consumption (Present)"
"label": "Alcohol Consumption (Present)",
"no_copy": 1
},
{
"fieldname": "column_break_32",
@ -309,13 +333,15 @@
"fieldname": "surrounding_factors",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Occupational Hazards and Environmental Factors"
"label": "Occupational Hazards and Environmental Factors",
"no_copy": 1
},
{
"fieldname": "other_risk_factors",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Other Risk Factors"
"label": "Other Risk Factors",
"no_copy": 1
},
{
"collapsible": 1,
@ -331,7 +357,8 @@
"fieldname": "patient_details",
"fieldtype": "Text",
"ignore_xss_filter": 1,
"label": "Patient Details"
"label": "Patient Details",
"no_copy": 1
},
{
"fieldname": "default_currency",
@ -342,19 +369,22 @@
{
"fieldname": "last_name",
"fieldtype": "Data",
"label": "Last Name"
"label": "Last Name",
"no_copy": 1
},
{
"fieldname": "first_name",
"fieldtype": "Data",
"label": "First Name",
"no_copy": 1,
"oldfieldtype": "Data",
"reqd": 1
},
{
"fieldname": "middle_name",
"fieldtype": "Data",
"label": "Middle Name (optional)"
"label": "Middle Name (optional)",
"no_copy": 1
},
{
"collapsible": 1,
@ -389,13 +419,63 @@
"fieldtype": "Link",
"label": "Print Language",
"options": "Language"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "address_contacts",
"fieldtype": "Section Break",
"label": "Address and Contact",
"options": "fa fa-map-marker"
},
{
"fieldname": "address_html",
"fieldtype": "HTML",
"label": "Address HTML",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
{
"fieldname": "contact_html",
"fieldtype": "HTML",
"label": "Contact HTML",
"no_copy": 1,
"read_only": 1
},
{
"allow_in_quick_entry": 1,
"default": "1",
"fieldname": "invite_user",
"fieldtype": "Check",
"label": "Invite as User",
"no_copy": 1,
"read_only_depends_on": "eval: doc.user_id"
},
{
"fieldname": "user_id",
"fieldtype": "Read Only",
"label": "User ID",
"no_copy": 1,
"options": "User"
},
{
"allow_in_quick_entry": 1,
"bold": 1,
"fieldname": "uid",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Identification Number (UID)",
"unique": 1
}
],
"icon": "fa fa-user",
"image_field": "image",
"links": [],
"max_attachments": 50,
"modified": "2020-04-25 17:24:32.146415",
"modified": "2021-03-14 13:21:09.759906",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient",
@ -453,7 +533,7 @@
],
"quick_entry": 1,
"restrict_to_domain": "Healthcare",
"search_fields": "patient_name,mobile,email,phone",
"search_fields": "patient_name,mobile,email,phone,uid",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",

View File

@ -8,24 +8,27 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, cstr, getdate
import dateutil
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.contacts.doctype.contact.contact import get_default_contact
from frappe.model.naming import set_name_by_naming_series
from frappe.utils.nestedset import get_root_of
from erpnext import get_default_currency
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account, send_registration_sms
from erpnext.accounts.party import get_dashboard_info
class Patient(Document):
def onload(self):
'''Load address and contacts in `__onload`'''
load_address_and_contact(self)
self.load_dashboard_info()
def validate(self):
self.set_full_name()
self.add_as_website_user()
def before_insert(self):
self.set_missing_customer_details()
def after_insert(self):
self.add_as_website_user()
self.reload()
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient') and not self.customer:
create_customer(self)
if frappe.db.get_single_value('Healthcare Settings', 'collect_registration_fee'):
frappe.db.set_value('Patient', self.name, 'status', 'Disabled')
else:
@ -49,6 +52,16 @@ class Patient(Document):
else:
create_customer(self)
self.set_contact() # add or update contact
if not self.user_id and self.email and self.invite_user:
self.create_website_user()
def load_dashboard_info(self):
if self.customer:
info = get_dashboard_info('Customer', self.customer, None)
self.set_onload('dashboard_info', info)
def set_full_name(self):
if self.last_name:
self.patient_name = ' '.join(filter(None, [self.first_name, self.last_name]))
@ -71,18 +84,24 @@ class Patient(Document):
if not self.language:
self.language = frappe.db.get_single_value('System Settings', 'language')
def add_as_website_user(self):
if self.email:
if not frappe.db.exists ('User', self.email):
user = frappe.get_doc({
'doctype': 'User',
'first_name': self.first_name,
'last_name': self.last_name,
'email': self.email,
'user_type': 'Website User'
})
user.flags.ignore_permissions = True
user.add_roles('Patient')
def create_website_user(self):
if self.email and not frappe.db.exists('User', self.email):
user = frappe.get_doc({
'doctype': 'User',
'first_name': self.first_name,
'last_name': self.last_name,
'email': self.email,
'user_type': 'Website User',
'gender': self.sex,
'phone': self.phone,
'mobile_no': self.mobile,
'birth_date': self.dob
})
user.flags.ignore_permissions = True
user.enabled = True
user.send_welcome_email = True
user.add_roles('Patient')
frappe.db.set_value(self.doctype, self.name, 'user_id', user.name)
def autoname(self):
patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by')
@ -114,7 +133,7 @@ class Patient(Document):
age = self.age
if not age:
return
age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
age_str = f'{str(age.years)} {_("Years(s)")} {str(age.months)} {_("Month(s)")} {str(age.days)} {_("Day(s)")}'
return age_str
@frappe.whitelist()
@ -131,6 +150,58 @@ class Patient(Document):
return {'invoice': sales_invoice.name}
def set_contact(self):
if frappe.db.exists('Dynamic Link', {'parenttype':'Contact', 'link_doctype':'Patient', 'link_name':self.name}):
old_doc = self.get_doc_before_save()
if old_doc.email != self.email or old_doc.mobile != self.mobile or old_doc.phone != self.phone:
self.update_contact()
else:
self.reload()
if self.email or self.mobile or self.phone:
contact = frappe.get_doc({
'doctype': 'Contact',
'first_name': self.first_name,
'middle_name': self.middle_name,
'last_name': self.last_name,
'gender': self.sex,
'is_primary_contact': 1
})
contact.append('links', dict(link_doctype='Patient', link_name=self.name))
if self.customer:
contact.append('links', dict(link_doctype='Customer', link_name=self.customer))
contact.insert(ignore_permissions=True)
self.update_contact(contact) # update email, mobile and phone
def update_contact(self, contact=None):
if not contact:
contact_name = get_default_contact(self.doctype, self.name)
if contact_name:
contact = frappe.get_doc('Contact', contact_name)
if contact:
if self.email and self.email != contact.email_id:
for email in contact.email_ids:
email.is_primary = True if email.email_id == self.email else False
contact.add_email(self.email, is_primary=True)
contact.set_primary_email()
if self.mobile and self.mobile != contact.mobile_no:
for mobile in contact.phone_nos:
mobile.is_primary_mobile_no = True if mobile.phone == self.mobile else False
contact.add_phone(self.mobile, is_primary_mobile_no=True)
contact.set_primary('mobile_no')
if self.phone and self.phone != contact.phone:
for phone in contact.phone_nos:
phone.is_primary_phone = True if phone.phone == self.phone else False
contact.add_phone(self.phone, is_primary_phone=True)
contact.set_primary('phone')
contact.flags.ignore_validate = True # disable hook TODO: safe?
contact.save(ignore_permissions=True)
def create_customer(doc):
customer = frappe.get_doc({
'doctype': 'Customer',
@ -156,8 +227,8 @@ def make_invoice(patient, company):
sales_invoice.debit_to = get_receivable_account(company)
item_line = sales_invoice.append('items')
item_line.item_name = 'Registeration Fee'
item_line.description = 'Registeration Fee'
item_line.item_name = 'Registration Fee'
item_line.description = 'Registration Fee'
item_line.qty = 1
item_line.uom = uom
item_line.conversion_factor = 1
@ -181,8 +252,11 @@ def get_patient_detail(patient):
return details
def get_timeline_data(doctype, name):
"""Return timeline data from medical records"""
return dict(frappe.db.sql('''
'''
Return Patient's timeline data from medical records
Also include the associated Customer timeline data
'''
patient_timeline_data = dict(frappe.db.sql('''
SELECT
unix_timestamp(communication_date), count(*)
FROM
@ -191,3 +265,11 @@ def get_timeline_data(doctype, name):
patient=%s
and `communication_date` > date_sub(curdate(), interval 1 year)
GROUP BY communication_date''', name))
customer = frappe.db.get_value(doctype, name, 'customer')
if customer:
from erpnext.accounts.party import get_timeline_data
customer_timeline_data = get_timeline_data('Customer', customer)
patient_timeline_data.update(customer_timeline_data)
return patient_timeline_data

View File

@ -6,22 +6,33 @@ def get_data():
'heatmap': True,
'heatmap_message': _('This is based on transactions against this Patient. See timeline below for details'),
'fieldname': 'patient',
'non_standard_fieldnames': {
'Payment Entry': 'party'
},
'transactions': [
{
'label': _('Appointments and Patient Encounters'),
'items': ['Patient Appointment', 'Patient Encounter']
'label': _('Appointments and Encounters'),
'items': ['Patient Appointment', 'Vital Signs', 'Patient Encounter']
},
{
'label': _('Lab Tests and Vital Signs'),
'items': ['Lab Test', 'Sample Collection', 'Vital Signs']
'items': ['Lab Test', 'Sample Collection']
},
{
'label': _('Billing'),
'items': ['Sales Invoice']
'label': _('Rehab and Physiotherapy'),
'items': ['Patient Assessment', 'Therapy Session', 'Therapy Plan']
},
{
'label': _('Orders'),
'items': ['Inpatient Medication Order']
'label': _('Surgery'),
'items': ['Clinical Procedure']
},
{
'label': _('Admissions'),
'items': ['Inpatient Record', 'Inpatient Medication Order']
},
{
'label': _('Billing and Payments'),
'items': ['Sales Invoice', 'Payment Entry']
}
]
}

View File

@ -17,9 +17,9 @@ frappe.ui.form.on('Patient Appointment', {
},
refresh: function(frm) {
frm.set_query('patient', function () {
frm.set_query('patient', function() {
return {
filters: {'status': 'Active'}
filters: { 'status': 'Active' }
};
});
@ -64,7 +64,7 @@ frappe.ui.form.on('Patient Appointment', {
} else {
frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd',
args: {'patient': frm.doc.patient},
args: { 'patient': frm.doc.patient },
callback: function(data) {
if (data.message == true) {
if (frm.doc.mode_of_payment && frm.doc.paid_amount) {
@ -97,7 +97,7 @@ frappe.ui.form.on('Patient Appointment', {
if (frm.doc.patient) {
frm.add_custom_button(__('Patient History'), function() {
frappe.route_options = {'patient': frm.doc.patient};
frappe.route_options = { 'patient': frm.doc.patient };
frappe.set_route('patient_history');
}, __('View'));
}
@ -111,14 +111,14 @@ frappe.ui.form.on('Patient Appointment', {
});
if (frm.doc.procedure_template) {
frm.add_custom_button(__('Clinical Procedure'), function(){
frm.add_custom_button(__('Clinical Procedure'), function() {
frappe.model.open_mapped_doc({
method: 'erpnext.healthcare.doctype.clinical_procedure.clinical_procedure.make_procedure',
frm: frm,
});
}, __('Create'));
} else if (frm.doc.therapy_type) {
frm.add_custom_button(__('Therapy Session'),function(){
frm.add_custom_button(__('Therapy Session'), function() {
frappe.model.open_mapped_doc({
method: 'erpnext.healthcare.doctype.therapy_session.therapy_session.create_therapy_session',
frm: frm,
@ -148,7 +148,7 @@ frappe.ui.form.on('Patient Appointment', {
doctype: 'Patient',
name: frm.doc.patient
},
callback: function (data) {
callback: function(data) {
let age = null;
if (data.message.dob) {
age = calculate_age(data.message.dob);
@ -165,7 +165,7 @@ frappe.ui.form.on('Patient Appointment', {
},
practitioner: function(frm) {
if (frm.doc.practitioner ) {
if (frm.doc.practitioner) {
frm.events.set_payment_details(frm);
}
},
@ -230,7 +230,7 @@ frappe.ui.form.on('Patient Appointment', {
toggle_payment_fields: function(frm) {
frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd',
args: {'patient': frm.doc.patient},
args: { 'patient': frm.doc.patient },
callback: function(data) {
if (data.message.fee_validity) {
// if fee validity exists and automated appointment invoicing is enabled,
@ -254,7 +254,7 @@ frappe.ui.form.on('Patient Appointment', {
frm.toggle_display('paid_amount', data.message ? 1 : 0);
frm.toggle_display('billing_item', data.message ? 1 : 0);
frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0);
frm.toggle_reqd('paid_amount', data.message ? 1 :0);
frm.toggle_reqd('paid_amount', data.message ? 1 : 0);
frm.toggle_reqd('billing_item', data.message ? 1 : 0);
}
}
@ -265,7 +265,7 @@ frappe.ui.form.on('Patient Appointment', {
if (frm.doc.patient) {
frappe.call({
method: "erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_prescribed_therapies",
args: {patient: frm.doc.patient},
args: { patient: frm.doc.patient },
callback: function(r) {
if (r.message) {
show_therapy_types(frm, r.message);
@ -302,13 +302,13 @@ let check_and_set_availability = function(frm) {
let d = new frappe.ui.Dialog({
title: __('Available slots'),
fields: [
{ fieldtype: 'Link', options: 'Medical Department', reqd: 1, fieldname: 'department', label: 'Medical Department'},
{ fieldtype: 'Column Break'},
{ fieldtype: 'Link', options: 'Healthcare Practitioner', reqd: 1, fieldname: 'practitioner', label: 'Healthcare Practitioner'},
{ fieldtype: 'Column Break'},
{ fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date'},
{ fieldtype: 'Section Break'},
{ fieldtype: 'HTML', fieldname: 'available_slots'}
{ fieldtype: 'Link', options: 'Medical Department', reqd: 1, fieldname: 'department', label: 'Medical Department' },
{ fieldtype: 'Column Break' },
{ fieldtype: 'Link', options: 'Healthcare Practitioner', reqd: 1, fieldname: 'practitioner', label: 'Healthcare Practitioner' },
{ fieldtype: 'Column Break' },
{ fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date' },
{ fieldtype: 'Section Break' },
{ fieldtype: 'HTML', fieldname: 'available_slots' }
],
primary_action_label: __('Book'),
@ -386,59 +386,22 @@ let check_and_set_availability = function(frm) {
let $wrapper = d.fields_dict.available_slots.$wrapper;
// make buttons for each slot
let slot_details = data.slot_details;
let slot_html = '';
for (let i = 0; i < slot_details.length; i++) {
slot_html = slot_html + `<label>${slot_details[i].slot_name}</label>`;
slot_html = slot_html + `<br/>` + slot_details[i].avail_slot.map(slot => {
let disabled = '';
let start_str = slot.from_time;
let slot_start_time = moment(slot.from_time, 'HH:mm:ss');
let slot_to_time = moment(slot.to_time, 'HH:mm:ss');
let interval = (slot_to_time - slot_start_time)/60000 | 0;
// iterate in all booked appointments, update the start time and duration
slot_details[i].appointments.forEach(function(booked) {
let booked_moment = moment(booked.appointment_time, 'HH:mm:ss');
let end_time = booked_moment.clone().add(booked.duration, 'minutes');
// Deal with 0 duration appointments
if (booked_moment.isSame(slot_start_time) || booked_moment.isBetween(slot_start_time, slot_to_time)) {
if(booked.duration == 0){
disabled = 'disabled="disabled"';
return false;
}
}
// Check for overlaps considering appointment duration
if (slot_start_time.isBefore(end_time) && slot_to_time.isAfter(booked_moment)) {
// There is an overlap
disabled = 'disabled="disabled"';
return false;
}
});
return `<button class="btn btn-default"
data-name=${start_str}
data-duration=${interval}
data-service-unit="${slot_details[i].service_unit || ''}"
style="margin: 0 10px 10px 0; width: 72px;" ${disabled}>
${start_str.substring(0, start_str.length - 3)}
</button>`;
}).join("");
slot_html = slot_html + `<br/>`;
}
let slot_html = get_slots(data.slot_details);
$wrapper
.css('margin-bottom', 0)
.addClass('text-center')
.html(slot_html);
// blue button when clicked
// highlight button when clicked
$wrapper.on('click', 'button', function() {
let $btn = $(this);
$wrapper.find('button').removeClass('btn-primary');
$btn.addClass('btn-primary');
$wrapper.find('button').removeClass('btn-outline-primary');
$btn.addClass('btn-outline-primary');
selected_slot = $btn.attr('data-name');
service_unit = $btn.attr('data-service-unit');
duration = $btn.attr('data-duration');
// enable dialog action
// enable primary action 'Book'
d.get_primary_btn().attr('disabled', null);
});
@ -448,19 +411,102 @@ let check_and_set_availability = function(frm) {
}
},
freeze: true,
freeze_message: __('Fetching records......')
freeze_message: __('Fetching Schedule...')
});
} else {
fd.available_slots.html(__('Appointment date and Healthcare Practitioner are Mandatory').bold());
}
}
function get_slots(slot_details) {
let slot_html = '';
let appointment_count = 0;
let disabled = false;
let start_str, slot_start_time, slot_end_time, interval, count, count_class, tool_tip, available_slots;
slot_details.forEach((slot_info) => {
slot_html += `<div class="slot-info">
<span> <b> ${__('Practitioner Schedule:')} </b> ${slot_info.slot_name} </span><br>
<span> <b> ${__('Service Unit:')} </b> ${slot_info.service_unit} </span>`;
if (slot_info.service_unit_capacity) {
slot_html += `<br><span> <b> ${__('Maximum Capacity:')} </b> ${slot_info.service_unit_capacity} </span>`;
}
slot_html += '</div><br><br>';
slot_html += slot_info.avail_slot.map(slot => {
appointment_count = 0;
disabled = false;
start_str = slot.from_time;
slot_start_time = moment(slot.from_time, 'HH:mm:ss');
slot_end_time = moment(slot.to_time, 'HH:mm:ss');
interval = (slot_end_time - slot_start_time) / 60000 | 0;
// iterate in all booked appointments, update the start time and duration
slot_info.appointments.forEach((booked) => {
let booked_moment = moment(booked.appointment_time, 'HH:mm:ss');
let end_time = booked_moment.clone().add(booked.duration, 'minutes');
// Deal with 0 duration appointments
if (booked_moment.isSame(slot_start_time) || booked_moment.isBetween(slot_start_time, slot_end_time)) {
if (booked.duration == 0) {
disabled = true;
return false;
}
}
// Check for overlaps considering appointment duration
if (slot_info.allow_overlap != 1) {
if (slot_start_time.isBefore(end_time) && slot_end_time.isAfter(booked_moment)) {
// There is an overlap
disabled = true;
return false;
}
} else {
if (slot_start_time.isBefore(end_time) && slot_end_time.isAfter(booked_moment)) {
appointment_count++;
}
if (appointment_count >= slot_info.service_unit_capacity) {
// There is an overlap
disabled = true;
return false;
}
}
});
if (slot_info.allow_overlap == 1 && slot_info.service_unit_capacity > 1) {
available_slots = slot_info.service_unit_capacity - appointment_count;
count = `${(available_slots > 0 ? available_slots : __('Full'))}`;
count_class = `${(available_slots > 0 ? 'badge-success' : 'badge-danger')}`;
tool_tip =`${available_slots} ${__('slots available for booking')}`;
}
return `
<button class="btn btn-secondary" data-name=${start_str}
data-duration=${interval}
data-service-unit="${slot_info.service_unit || ''}"
style="margin: 0 10px 10px 0; width: auto;" ${disabled ? 'disabled="disabled"' : ""}
data-toggle="tooltip" title="${tool_tip}">
${start_str.substring(0, start_str.length - 3)}<br>
<span class='badge ${count_class}'> ${count} </span>
</button>`;
}).join("");
if (slot_info.service_unit_capacity) {
slot_html += `<br/><small>${__('Each slot indicates the capacity currently available for booking')}</small>`;
}
slot_html += `<br/><br/>`;
});
return slot_html;
}
};
let get_prescribed_procedure = function(frm) {
if (frm.doc.patient) {
frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_procedure_prescribed',
args: {patient: frm.doc.patient},
args: { patient: frm.doc.patient },
callback: function(r) {
if (r.message && r.message.length) {
show_procedure_templates(frm, r.message);
@ -480,7 +526,7 @@ let get_prescribed_procedure = function(frm) {
}
};
let show_procedure_templates = function(frm, result){
let show_procedure_templates = function(frm, result) {
let d = new frappe.ui.Dialog({
title: __('Prescribed Procedures'),
fields: [
@ -500,9 +546,11 @@ let show_procedure_templates = function(frm, result){
data-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\
data-date="%(date)s" data-department="%(department)s">\
<button class="btn btn-default btn-xs">Add\
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {name:y[0], procedure_template: y[1],
encounter:y[2], consulting_practitioner:y[3], encounter_date:y[4],
practitioner:y[5]? y[5]:'', date: y[6]? y[6]:'', department: y[7]? y[7]:''})).appendTo(html_field);
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {
name: y[0], procedure_template: y[1],
encounter: y[2], consulting_practitioner: y[3], encounter_date: y[4],
practitioner: y[5] ? y[5] : '', date: y[6] ? y[6] : '', department: y[7] ? y[7] : ''
})).appendTo(html_field);
row.find("a").click(function() {
frm.doc.procedure_template = $(this).attr('data-procedure-template');
frm.doc.procedure_prescription = $(this).attr('data-name');
@ -520,7 +568,7 @@ let show_procedure_templates = function(frm, result){
});
if (!result) {
let msg = __('There are no procedure prescribed for ') + frm.doc.patient;
$(repl('<div class="col-xs-12" style="padding-top:20px;" >%(msg)s</div></div>', {msg: msg})).appendTo(html_field);
$(repl('<div class="col-xs-12" style="padding-top:20px;" >%(msg)s</div></div>', { msg: msg })).appendTo(html_field);
}
d.show();
};
@ -535,7 +583,7 @@ let show_therapy_types = function(frm, result) {
]
});
var html_field = d.fields_dict.therapy_type.$wrapper;
$.each(result, function(x, y){
$.each(result, function(x, y) {
var row = $(repl('<div class="col-xs-12" style="padding-top:12px; text-align:center;" >\
<div class="col-xs-5"> %(encounter)s <br> %(practitioner)s <br> %(date)s </div>\
<div class="col-xs-5"> %(therapy)s </div>\
@ -544,9 +592,11 @@ let show_therapy_types = function(frm, result) {
data-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\
data-date="%(date)s" data-department="%(department)s">\
<button class="btn btn-default btn-xs">Add\
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {therapy:y[0],
name: y[1], encounter:y[2], practitioner:y[3], date:y[4],
department:y[6]? y[6]:'', therapy_plan:y[5]})).appendTo(html_field);
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {
therapy: y[0],
name: y[1], encounter: y[2], practitioner: y[3], date: y[4],
department: y[6] ? y[6] : '', therapy_plan: y[5]
})).appendTo(html_field);
row.find("a").click(function() {
frm.doc.therapy_type = $(this).attr("data-therapy");
@ -581,13 +631,13 @@ let create_vital_signs = function(frm) {
frappe.new_doc('Vital Signs');
};
let update_status = function(frm, status){
let update_status = function(frm, status) {
let doc = frm.doc;
frappe.confirm(__('Are you sure you want to cancel this appointment?'),
function() {
frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_status',
args: {appointment_id: doc.name, status:status},
args: { appointment_id: doc.name, status: status },
callback: function(data) {
if (!data.exc) {
frm.reload_doc();

View File

@ -131,7 +131,7 @@
"fieldtype": "Link",
"label": "Service Unit",
"options": "Healthcare Service Unit",
"set_only_once": 1
"read_only": 1
},
{
"depends_on": "eval:doc.practitioner;",
@ -349,7 +349,7 @@
}
],
"links": [],
"modified": "2021-06-16 00:40:26.841794",
"modified": "2021-08-30 09:00:41.329387",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Appointment",

View File

@ -15,6 +15,11 @@ from erpnext.hr.doctype.employee.employee import is_holiday
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account
from erpnext.healthcare.utils import check_fee_validity, get_service_item_and_practitioner_charge, manage_fee_validity
class MaximumCapacityError(frappe.ValidationError):
pass
class OverlapError(frappe.ValidationError):
pass
class PatientAppointment(Document):
def validate(self):
self.validate_overlaps()
@ -49,26 +54,49 @@ class PatientAppointment(Document):
end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \
+ datetime.timedelta(minutes=flt(self.duration))
overlaps = frappe.db.sql("""
select
name, practitioner, patient, appointment_time, duration
from
`tabPatient Appointment`
where
appointment_date=%s and name!=%s and status NOT IN ("Closed", "Cancelled")
and (practitioner=%s or patient=%s) and
((appointment_time<%s and appointment_time + INTERVAL duration MINUTE>%s) or
(appointment_time>%s and appointment_time<%s) or
(appointment_time=%s))
""", (self.appointment_date, self.name, self.practitioner, self.patient,
self.appointment_time, end_time.time(), self.appointment_time, end_time.time(), self.appointment_time))
# all appointments for both patient and practitioner overlapping the duration of this appointment
overlapping_appointments = frappe.db.sql("""
SELECT
name, practitioner, patient, appointment_time, duration, service_unit
FROM
`tabPatient Appointment`
WHERE
appointment_date=%(appointment_date)s AND name!=%(name)s AND status NOT IN ("Closed", "Cancelled") AND
(practitioner=%(practitioner)s OR patient=%(patient)s) AND
((appointment_time<%(appointment_time)s AND appointment_time + INTERVAL duration MINUTE>%(appointment_time)s) OR
(appointment_time>%(appointment_time)s AND appointment_time<%(end_time)s) OR
(appointment_time=%(appointment_time)s))
""",
{
'appointment_date': self.appointment_date,
'name': self.name,
'practitioner': self.practitioner,
'patient': self.patient,
'appointment_time': self.appointment_time,
'end_time':end_time.time()
},
as_dict = True
)
if not overlapping_appointments:
return # No overlaps, nothing to validate!
if self.service_unit: # validate service unit capacity if overlap enabled
allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', self.service_unit,
['overlap_appointments', 'service_unit_capacity'])
if allow_overlap:
service_unit_appointments = list(filter(lambda appointment: appointment['service_unit'] == self.service_unit and
appointment['patient'] != self.patient, overlapping_appointments)) # if same patient already booked, it should be an overlap
if len(service_unit_appointments) >= (service_unit_capacity or 1):
frappe.throw(_("Not allowed, {} cannot exceed maximum capacity {}")
.format(frappe.bold(self.service_unit), frappe.bold(service_unit_capacity or 1)), MaximumCapacityError)
else: # service_unit_appointments within capacity, remove from overlapping_appointments
overlapping_appointments = [appointment for appointment in overlapping_appointments if appointment not in service_unit_appointments]
if overlapping_appointments:
frappe.throw(_("Not allowed, cannot overlap appointment {}")
.format(frappe.bold(', '.join([appointment['name'] for appointment in overlapping_appointments]))), OverlapError)
if overlaps:
overlapping_details = _('Appointment overlaps with ')
overlapping_details += "<b><a href='/app/Form/Patient Appointment/{0}'>{0}</a></b><br>".format(overlaps[0][0])
overlapping_details += _('{0} has appointment scheduled with {1} at {2} having {3} minute(s) duration.').format(
overlaps[0][1], overlaps[0][2], overlaps[0][3], overlaps[0][4])
frappe.throw(overlapping_details, title=_('Appointments Overlapping'))
def validate_service_unit(self):
if self.inpatient_record and self.service_unit:
@ -325,6 +353,8 @@ def get_available_slots(practitioner_doc, date):
if available_slots:
appointments = []
allow_overlap = 0
service_unit_capacity = 0
# fetch all appointments to practitioner by service unit
filters = {
'practitioner': practitioner,
@ -334,8 +364,8 @@ def get_available_slots(practitioner_doc, date):
}
if schedule_entry.service_unit:
slot_name = schedule_entry.schedule + ' - ' + schedule_entry.service_unit
allow_overlap = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, 'overlap_appointments')
slot_name = f'{schedule_entry.schedule}'
allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, ['overlap_appointments', 'service_unit_capacity'])
if not allow_overlap:
# fetch all appointments to service unit
filters.pop('practitioner')
@ -350,8 +380,8 @@ def get_available_slots(practitioner_doc, date):
filters=filters,
fields=['name', 'appointment_time', 'duration', 'status'])
slot_details.append({'slot_name':slot_name, 'service_unit':schedule_entry.service_unit,
'avail_slot':available_slots, 'appointments': appointments})
slot_details.append({'slot_name': slot_name, 'service_unit': schedule_entry.service_unit, 'avail_slot': available_slots,
'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity})
return slot_details

View File

@ -16,9 +16,11 @@ class TestPatientAppointment(unittest.TestCase):
frappe.db.sql("""delete from `tabFee Validity`""")
frappe.db.sql("""delete from `tabPatient Encounter`""")
make_pos_profile()
frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test %'""")
frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test Service Unit Type%'""")
def test_status(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
appointment = create_appointment(patient, practitioner, nowdate())
self.assertEqual(appointment.status, 'Open')
@ -30,7 +32,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
def test_start_encounter(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1)
appointment.reload()
@ -44,7 +46,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'))
def test_auto_invoicing(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
appointment = create_appointment(patient, practitioner, nowdate())
@ -60,13 +62,14 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
def test_auto_invoicing_based_on_department(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
medical_department = create_medical_department()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment_type = create_appointment_type()
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
invoice=1, appointment_type=appointment_type.name, department='_Test Medical Department')
invoice=1, appointment_type=appointment_type.name, department=medical_department)
appointment.reload()
self.assertEqual(appointment.invoiced, 1)
@ -78,7 +81,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
def test_auto_invoicing_according_to_appointment_type_charge(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
@ -104,7 +107,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertTrue(sales_invoice_name)
def test_appointment_cancel(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
appointment = create_appointment(patient, practitioner, nowdate())
fee_validity = frappe.db.get_value('Fee Validity', {'patient': patient, 'practitioner': practitioner})
@ -112,7 +115,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertTrue(fee_validity)
# first follow up appointment
appointment = create_appointment(patient, practitioner, nowdate())
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1))
self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), 1)
update_status(appointment.name, 'Cancelled')
@ -121,7 +124,7 @@ class TestPatientAppointment(unittest.TestCase):
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, nowdate(), invoice=1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1)
update_status(appointment.name, 'Cancelled')
# check invoice cancelled
sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
@ -133,7 +136,7 @@ class TestPatientAppointment(unittest.TestCase):
create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
@ -141,7 +144,7 @@ class TestPatientAppointment(unittest.TestCase):
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit)
@ -159,7 +162,7 @@ class TestPatientAppointment(unittest.TestCase):
create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
@ -167,10 +170,10 @@ class TestPatientAppointment(unittest.TestCase):
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
appointment_service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy for Appointment')
appointment_service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy for Appointment')
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0)
self.assertRaises(frappe.exceptions.ValidationError, appointment.save)
@ -192,7 +195,7 @@ class TestPatientAppointment(unittest.TestCase):
assert payment_required is True
def test_sales_invoice_should_be_generated_for_new_patient_appointment(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
invoice_count = frappe.db.count('Sales Invoice')
@ -203,10 +206,10 @@ class TestPatientAppointment(unittest.TestCase):
assert new_invoice_count == invoice_count + 1
def test_patient_appointment_should_consider_permissions_while_fetching_appointments(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
create_appointment(patient, practitioner, nowdate())
patient, medical_department, new_practitioner = create_healthcare_docs(practitioner_name='Dr. John')
patient, new_practitioner = create_healthcare_docs(id=5)
create_appointment(patient, new_practitioner, nowdate())
roles = [{"doctype": "Has Role", "role": "Physician"}]
@ -223,41 +226,102 @@ class TestPatientAppointment(unittest.TestCase):
appointments = frappe.get_list('Patient Appointment')
assert len(appointments) == 2
def create_healthcare_docs(practitioner_name=None):
if not practitioner_name:
practitioner_name = '_Test Healthcare Practitioner'
def test_overlap_appointment(self):
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import OverlapError
patient, practitioner = create_healthcare_docs(id=1)
patient_1, practitioner_1 = create_healthcare_docs(id=2)
service_unit = create_service_unit(id=0)
service_unit_1 = create_service_unit(id=1)
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit) # valid
patient = create_patient()
practitioner = frappe.db.exists('Healthcare Practitioner', practitioner_name)
medical_department = frappe.db.exists('Medical Department', '_Test Medical Department')
# patient and practitioner cannot have overlapping appointments
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit_1, save=0) # diff service unit
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner, nowdate(), save=0) # with no service unit link
self.assertRaises(OverlapError, appointment.save)
if not medical_department:
medical_department = frappe.new_doc('Medical Department')
medical_department.department = '_Test Medical Department'
medical_department.save(ignore_permissions=True)
medical_department = medical_department.name
# patient cannot have overlapping appointments with other practitioners
appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit_1, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner_1, nowdate(), save=0)
self.assertRaises(OverlapError, appointment.save)
if not practitioner:
practitioner = frappe.new_doc('Healthcare Practitioner')
practitioner.first_name = practitioner_name
practitioner.gender = 'Female'
practitioner.department = medical_department
practitioner.op_consulting_charge = 500
practitioner.inpatient_visit_charge = 500
practitioner.save(ignore_permissions=True)
practitioner = practitioner.name
# practitioner cannot have overlapping appointments with other patients
appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit_1, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient_1, practitioner, nowdate(), save=0)
self.assertRaises(OverlapError, appointment.save)
return patient, medical_department, practitioner
def test_service_unit_capacity(self):
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import MaximumCapacityError, OverlapError
practitioner = create_practitioner()
capacity = 3
overlap_service_unit_type = create_service_unit_type(id=10, allow_appointments=1, overlap_appointments=1)
overlap_service_unit = create_service_unit(id=100, service_unit_type=overlap_service_unit_type, service_unit_capacity=capacity)
for i in range(0, capacity):
patient = create_patient(id=i)
create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit) # valid
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0) # overlap
self.assertRaises(OverlapError, appointment.save)
patient = create_patient(id=capacity)
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0)
self.assertRaises(MaximumCapacityError, appointment.save)
def create_healthcare_docs(id=0):
patient = create_patient(id)
practitioner = create_practitioner(id)
return patient, practitioner
def create_patient(id=0):
if frappe.db.exists('Patient', {'firstname':f'_Test Patient {str(id)}'}):
patient = frappe.db.get_value('Patient', {'first_name': f'_Test Patient {str(id)}'}, ['name'])
return patient
patient = frappe.new_doc('Patient')
patient.first_name = f'_Test Patient {str(id)}'
patient.sex = 'Female'
patient.save(ignore_permissions=True)
return patient.name
def create_medical_department(id=0):
if frappe.db.exists('Medical Department', f'_Test Medical Department {str(id)}'):
return f'_Test Medical Department {str(id)}'
medical_department = frappe.new_doc('Medical Department')
medical_department.department = f'_Test Medical Department {str(id)}'
medical_department.save(ignore_permissions=True)
return medical_department.name
def create_practitioner(id=0, medical_department=None):
if frappe.db.exists('Healthcare Practitioner', {'firstname':f'_Test Healthcare Practitioner {str(id)}'}):
practitioner = frappe.db.get_value('Healthcare Practitioner', {'firstname':f'_Test Healthcare Practitioner {str(id)}'}, ['name'])
return practitioner
practitioner = frappe.new_doc('Healthcare Practitioner')
practitioner.first_name = f'_Test Healthcare Practitioner {str(id)}'
practitioner.gender = 'Female'
practitioner.department = medical_department or create_medical_department(id)
practitioner.op_consulting_charge = 500
practitioner.inpatient_visit_charge = 500
practitioner.save(ignore_permissions=True)
return practitioner.name
def create_patient():
patient = frappe.db.exists('Patient', '_Test Patient')
if not patient:
patient = frappe.new_doc('Patient')
patient.first_name = '_Test Patient'
patient.sex = 'Female'
patient.save(ignore_permissions=True)
patient = patient.name
return patient
def create_encounter(appointment):
if appointment:
@ -270,8 +334,10 @@ def create_encounter(appointment):
encounter.company = appointment.company
encounter.save()
encounter.submit()
return encounter
def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0,
service_unit=None, appointment_type=None, save=1, department=None):
item = create_healthcare_service_items()
@ -284,6 +350,7 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.appointment_date = appointment_date
appointment.company = '_Test Company'
appointment.duration = 15
if service_unit:
appointment.service_unit = service_unit
if invoice:
@ -294,11 +361,14 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.procedure_template = create_clinical_procedure_template().get('name')
if save:
appointment.save(ignore_permissions=True)
return appointment
def create_healthcare_service_items():
if frappe.db.exists('Item', 'HLC-SI-001'):
return 'HLC-SI-001'
item = frappe.new_doc('Item')
item.item_code = 'HLC-SI-001'
item.item_name = 'Consulting Charges'
@ -306,11 +376,14 @@ def create_healthcare_service_items():
item.is_stock_item = 0
item.stock_uom = 'Nos'
item.save()
return item.name
def create_clinical_procedure_template():
if frappe.db.exists('Clinical Procedure Template', 'Knee Surgery and Rehab'):
return frappe.get_doc('Clinical Procedure Template', 'Knee Surgery and Rehab')
template = frappe.new_doc('Clinical Procedure Template')
template.template = 'Knee Surgery and Rehab'
template.item_code = 'Knee Surgery and Rehab'
@ -319,8 +392,10 @@ def create_clinical_procedure_template():
template.description = 'Knee Surgery and Rehab'
template.rate = 50000
template.save()
return template
def create_appointment_type(args=None):
if not args:
args = frappe.local.form_dict
@ -359,3 +434,30 @@ def create_user(email=None, roles=None):
"roles": roles,
}).insert()
return user
def create_service_unit_type(id=0, allow_appointments=1, overlap_appointments=0):
if frappe.db.exists('Healthcare Service Unit Type', f'_Test Service Unit Type {str(id)}'):
return f'_Test Service Unit Type {str(id)}'
service_unit_type = frappe.new_doc('Healthcare Service Unit Type')
service_unit_type.service_unit_type = f'_Test Service Unit Type {str(id)}'
service_unit_type.allow_appointments = allow_appointments
service_unit_type.overlap_appointments = overlap_appointments
service_unit_type.save(ignore_permissions=True)
return service_unit_type.name
def create_service_unit(id=0, service_unit_type=None, service_unit_capacity=0):
if frappe.db.exists('Healthcare Service Unit', f'_Test Service Unit {str(id)}'):
return f'_Test service_unit {str(id)}'
service_unit = frappe.new_doc('Healthcare Service Unit')
service_unit.is_group = 0
service_unit.healthcare_service_unit_name= f'_Test Service Unit {str(id)}'
service_unit.service_unit_type = service_unit_type or create_service_unit_type(id)
service_unit.service_unit_capacity = service_unit_capacity
service_unit.save(ignore_permissions=True)
return service_unit.name

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import nowdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment, create_medical_department
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
class TestPatientMedicalRecord(unittest.TestCase):
@ -15,7 +15,8 @@ class TestPatientMedicalRecord(unittest.TestCase):
make_pos_profile()
def test_medical_record(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
medical_department = create_medical_department()
appointment = create_appointment(patient, practitioner, nowdate(), invoice=1)
encounter = create_encounter(appointment)

View File

@ -8,11 +8,13 @@ import unittest
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, create_appointment
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import \
create_healthcare_docs, create_patient, create_appointment, create_medical_department
class TestTherapyPlan(unittest.TestCase):
def test_creation_on_encounter_submission(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
medical_department = create_medical_department()
encounter = create_encounter(patient, medical_department, practitioner)
self.assertTrue(frappe.db.exists('Therapy Plan', encounter.therapy_plan))
@ -28,8 +30,9 @@ class TestTherapyPlan(unittest.TestCase):
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
patient, medical_department, practitioner = create_healthcare_docs()
appointment = create_appointment(patient, practitioner, nowdate())
patient, 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()

View File

@ -34,7 +34,8 @@ def create_therapy_type():
})
therapy_type.save()
else:
therapy_type = frappe.get_doc('Therapy Type', 'Basic Rehab')
therapy_type = frappe.get_doc('Therapy Type', therapy_type)
return therapy_type
def create_exercise_type():
@ -47,4 +48,7 @@ def create_exercise_type():
'description': 'Squat and Rise'
})
exercise_type.save()
else:
exercise_type = frappe.get_doc('Exercise Type', exercise_type)
return exercise_type

View File

@ -25,7 +25,7 @@ class TestInpatientMedicationOrders(unittest.TestCase):
'from_date': getdate(),
'to_date': getdate(),
'patient': '_Test IPD Patient',
'service_unit': 'Test Service Unit Ip Occupancy - _TC'
'service_unit': '_Test Service Unit Ip Occupancy - _TC'
}
report = execute(filters)
@ -42,7 +42,7 @@ class TestInpatientMedicationOrders(unittest.TestCase):
'date': getdate(),
'time': datetime.timedelta(seconds=32400),
'is_completed': 0,
'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC'
},
{
'patient': '_Test IPD Patient',
@ -55,7 +55,7 @@ class TestInpatientMedicationOrders(unittest.TestCase):
'date': getdate(),
'time': datetime.timedelta(seconds=50400),
'is_completed': 0,
'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC'
},
{
'patient': '_Test IPD Patient',
@ -68,7 +68,7 @@ class TestInpatientMedicationOrders(unittest.TestCase):
'date': getdate(),
'time': datetime.timedelta(seconds=75600),
'is_completed': 0,
'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC'
}
]
@ -83,7 +83,7 @@ class TestInpatientMedicationOrders(unittest.TestCase):
'from_date': getdate(),
'to_date': getdate(),
'patient': '_Test IPD Patient',
'service_unit': 'Test Service Unit Ip Occupancy - _TC',
'service_unit': '_Test Service Unit Ip Occupancy - _TC',
'show_completed_orders': 0
}
@ -119,7 +119,7 @@ def create_records(patient):
ip_record.expected_length_of_stay = 0
ip_record.save()
ip_record.reload()
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
ipmo = create_ipmo(patient)

View File

@ -543,58 +543,43 @@ def get_drugs_to_invoice(encounter):
@frappe.whitelist()
def get_children(doctype, parent, company, is_root=False):
parent_fieldname = "parent_" + doctype.lower().replace(" ", "_")
def get_children(doctype, parent=None, company=None, is_root=False):
parent_fieldname = 'parent_' + doctype.lower().replace(' ', '_')
fields = [
"name as value",
"is_group as expandable",
"lft",
"rgt"
'name as value',
'is_group as expandable',
'lft',
'rgt'
]
# fields = [ "name", "is_group", "lft", "rgt" ]
filters = [["ifnull(`{0}`,'')".format(parent_fieldname), "=", "" if is_root else parent]]
filters = [["ifnull(`{0}`,'')".format(parent_fieldname),
'=', '' if is_root else parent]]
if is_root:
fields += ["service_unit_type"] if doctype == "Healthcare Service Unit" else []
filters.append(["company", "=", company])
fields += ['service_unit_type'] if doctype == 'Healthcare Service Unit' else []
filters.append(['company', '=', company])
else:
fields += ["service_unit_type", "allow_appointments", "inpatient_occupancy", "occupancy_status"] if doctype == "Healthcare Service Unit" else []
fields += [parent_fieldname + " as parent"]
fields += ['service_unit_type', 'allow_appointments', 'inpatient_occupancy',
'occupancy_status'] if doctype == 'Healthcare Service Unit' else []
fields += [parent_fieldname + ' as parent']
hc_service_units = frappe.get_list(doctype, fields=fields, filters=filters)
service_units = frappe.get_list(doctype, fields=fields, filters=filters)
for each in service_units:
if each['expandable'] == 1: # group node
available_count = frappe.db.count('Healthcare Service Unit', filters={
'parent_healthcare_service_unit': each['value'],
'inpatient_occupancy': 1})
if doctype == "Healthcare Service Unit":
for each in hc_service_units:
occupancy_msg = ""
if each["expandable"] == 1:
occupied = False
vacant = False
child_list = frappe.db.sql(
'''
SELECT
name, occupancy_status
FROM
`tabHealthcare Service Unit`
WHERE
inpatient_occupancy = 1
and lft > %s and rgt < %s
''', (each['lft'], each['rgt']))
if available_count > 0:
occupied_count = frappe.db.count('Healthcare Service Unit', {
'parent_healthcare_service_unit': each['value'],
'inpatient_occupancy': 1,
'occupancy_status': 'Occupied'})
# set occupancy status of group node
each['occupied_of_available'] = str(
occupied_count) + ' Occupied of ' + str(available_count)
for child in child_list:
if not occupied:
occupied = 0
if child[1] == "Occupied":
occupied += 1
if not vacant:
vacant = 0
if child[1] == "Vacant":
vacant += 1
if vacant and occupied:
occupancy_total = vacant + occupied
occupancy_msg = str(occupied) + " Occupied out of " + str(occupancy_total)
each["occupied_out_of_vacant"] = occupancy_msg
return hc_service_units
return service_units
@frappe.whitelist()
@ -717,3 +702,40 @@ def render_doc_as_html(doctype, docname, exclude_fields = []):
doc_html = "<div class='small'><div class='col-md-12 text-right'><a class='btn btn-default btn-xs' href='/app/Form/%s/%s'></a></div>" %(doctype, docname) + doc_html + '</div>'
return {'html': doc_html}
def update_address_links(address, method):
'''
Hook validate Address
If Patient is linked in Address, also link the associated Customer
'''
if 'Healthcare' not in frappe.get_active_domains():
return
patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', address.links))
for link in patient_links:
customer = frappe.db.get_value('Patient', link.get('link_name'), 'customer')
if customer and not address.has_link('Customer', customer):
address.append('links', dict(link_doctype = 'Customer', link_name = customer))
def update_patient_email_and_phone_numbers(contact, method):
'''
Hook validate Contact
Update linked Patients' primary mobile and phone numbers
'''
if 'Healthcare' not in frappe.get_active_domains():
return
if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone):
patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', contact.links))
for link in patient_links:
contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1)
if contact.email_id and contact.email_id != contact_details.get('email'):
frappe.db.set_value('Patient', link.get('link_name'), 'email', contact.email_id)
if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'):
frappe.db.set_value('Patient', link.get('link_name'), 'mobile', contact.mobile_no)
if contact.phone and contact.phone != contact_details.get('phone'):
frappe.db.set_value('Patient', link.get('link_name'), 'phone', contact.phone)

View File

@ -290,7 +290,12 @@ doc_events = {
"on_trash": "erpnext.regional.check_deletion_permission"
},
'Address': {
'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code', 'erpnext.regional.india.utils.update_gst_category']
'validate': [
'erpnext.regional.india.utils.validate_gstin_for_india',
'erpnext.regional.italy.utils.set_state_code',
'erpnext.regional.india.utils.update_gst_category',
'erpnext.healthcare.utils.update_address_links'
],
},
'Supplier': {
'validate': 'erpnext.regional.india.utils.validate_pan_for_india'
@ -301,7 +306,7 @@ doc_events = {
"Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",
"validate": "erpnext.crm.utils.update_lead_phone_numbers"
"validate": ["erpnext.crm.utils.update_lead_phone_numbers", "erpnext.healthcare.utils.update_patient_email_and_phone_numbers"]
},
"Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"