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>
diff --git a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py
index 81a3982..0326e5e 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py
+++ b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py
@@ -11,7 +11,7 @@
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 @@
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()
diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py
index 4a17872..957f852 100644
--- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py
+++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py
@@ -27,7 +27,7 @@
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())
diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.js b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.js
index 2cdd550..2d1caf7 100644
--- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.js
+++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.js
@@ -7,8 +7,8 @@
// 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 @@
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 @@
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', '');
+ }
}
});
diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json
index 9ee865a..8935ec7 100644
--- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json
+++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json
@@ -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": [
{
diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py
index 9e0417a..989d426 100644
--- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py
+++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py
@@ -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 @@
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
diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js
index b75f271..ea3fea6 100644
--- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js
+++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js
@@ -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
};
diff --git a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.js b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.js
index eb33ab6..ecf4aa1 100644
--- a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.js
+++ b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.js
@@ -68,8 +68,8 @@
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();
}
});
diff --git a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json
index 4b8503d..9c81c65 100644
--- a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json
+++ b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json
@@ -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",
diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py
index a8c7720..b4a9612 100644
--- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py
+++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py
@@ -151,7 +151,7 @@
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 @@
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 @@
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
diff --git a/erpnext/healthcare/doctype/patient/patient.js b/erpnext/healthcare/doctype/patient/patient.js
index bce42e5..9266467 100644
--- a/erpnext/healthcare/doctype/patient/patient.js
+++ b/erpnext/healthcare/doctype/patient/patient.js
@@ -26,31 +26,39 @@
}
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 @@
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('');
}
});
diff --git a/erpnext/healthcare/doctype/patient/patient.json b/erpnext/healthcare/doctype/patient/patient.json
index 8af1a9c..4092a6a 100644
--- a/erpnext/healthcare/doctype/patient/patient.json
+++ b/erpnext/healthcare/doctype/patient/patient.json
@@ -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",
diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py
index f77ad70..9dae1f6 100644
--- a/erpnext/healthcare/doctype/patient/patient.py
+++ b/erpnext/healthcare/doctype/patient/patient.py
@@ -8,24 +8,27 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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
diff --git a/erpnext/healthcare/doctype/patient/patient_dashboard.py b/erpnext/healthcare/doctype/patient/patient_dashboard.py
index 39603f7..7f7cfa8 100644
--- a/erpnext/healthcare/doctype/patient/patient_dashboard.py
+++ b/erpnext/healthcare/doctype/patient/patient_dashboard.py
@@ -6,22 +6,33 @@
'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']
}
]
}
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
index c6e489e..49847d5 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
@@ -17,9 +17,9 @@
},
refresh: function(frm) {
- frm.set_query('patient', function () {
+ frm.set_query('patient', function() {
return {
- filters: {'status': 'Active'}
+ filters: { 'status': 'Active' }
};
});
@@ -64,7 +64,7 @@
} 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 @@
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 @@
});
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 @@
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 @@
},
practitioner: function(frm) {
- if (frm.doc.practitioner ) {
+ if (frm.doc.practitioner) {
frm.events.set_payment_details(frm);
}
},
@@ -230,7 +230,7 @@
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 @@
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 @@
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 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 $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 @@
}
},
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 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 @@
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 @@
});
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 @@
]
});
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 @@
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 @@
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();
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
index 73ec3bc..28d3a6d 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
@@ -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",
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
index 7db4fa6..36047c4 100755
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
@@ -15,6 +15,11 @@
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 @@
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 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'))
+ 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)
+
def validate_service_unit(self):
if self.inpatient_record and self.service_unit:
@@ -325,6 +353,8 @@
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 @@
}
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 @@
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
diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
index 157b3e1..f5477c0 100644
--- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
@@ -16,9 +16,11 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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)
-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
+ 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_encounter(appointment):
if appointment:
@@ -270,8 +334,10 @@
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 @@
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 @@
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 @@
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 @@
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 @@
"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
diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
index f8ccc8a..5b7d8d6 100644
--- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
+++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
@@ -5,7 +5,7 @@
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 @@
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)
diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
index 113fa51..983fba9 100644
--- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
@@ -8,11 +8,13 @@
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 @@
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()
diff --git a/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py
index a5dad29..80fc83f 100644
--- a/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py
+++ b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py
@@ -34,7 +34,8 @@
})
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 @@
'description': 'Squat and Rise'
})
exercise_type.save()
+ else:
+ exercise_type = frappe.get_doc('Exercise Type', exercise_type)
+
return exercise_type
diff --git a/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py
index 4b461f1..fae5ece 100644
--- a/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py
+++ b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py
@@ -25,7 +25,7 @@
'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 @@
'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 @@
'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 @@
'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 @@
'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 @@
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)
diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py
index d3d22c8..ffecf4d 100644
--- a/erpnext/healthcare/utils.py
+++ b/erpnext/healthcare/utils.py
@@ -543,58 +543,43 @@
@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 @@
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)
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 4854bfd..b1a64f9 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -290,7 +290,12 @@
"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 @@
"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"