feat: Inpatient Medication Order and Entry (#23473)

* feat: Inpatient Medication Order

* feat: Inpatient Medication Entry

* feat: update medication orders on medication entry submission

* feat: added custom fields for medication references in Stock Entry

* feat: make stock entry if update stock is checked in IPMOE

* fix: handle cancel event for Inpatient Medication Entry

* fix(ux): add link and progress bar to dashboard

* refactor(ux): Adding Medication Orders without linking to Patient Encounter

* fix: make medication entry child table read only

* fix: filter stock items during manual medication order creation

* fix: codacy

* chore: tests for Inpatient Medication Order

* chore: tests for Inpatient Medication Entry

* fix: code clean-up

* fix: filter for inpatients in IPMO

* fix: add datetime validations for IPME filters

* fix: do not hardcode stock entry type

Co-authored-by: Nabin Hait <nabinhait@gmail.com>
This commit is contained in:
Rucha Mahabal 2020-10-23 20:33:30 +05:30 committed by GitHub
parent 3ad9393ff8
commit 4887b69946
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1596 additions and 14 deletions

View File

@ -49,6 +49,22 @@ data = {
'fieldname': 'reference_dn', 'label': 'Reference Name', 'fieldtype': 'Dynamic Link', 'options': 'reference_dt',
'insert_after': 'reference_dt'
}
],
'Stock Entry': [
{
'fieldname': 'inpatient_medication_entry', 'label': 'Inpatient Medication Entry', 'fieldtype': 'Link', 'options': 'Inpatient Medication Entry',
'insert_after': 'credit_note', 'read_only': True
}
],
'Stock Entry Detail': [
{
'fieldname': 'patient', 'label': 'Patient', 'fieldtype': 'Link', 'options': 'Patient',
'insert_after': 'po_detail', 'read_only': True
},
{
'fieldname': 'inpatient_medication_entry_child', 'label': 'Inpatient Medication Entry Child', 'fieldtype': 'Data',
'insert_after': 'patient', 'read_only': True
}
]
},
'on_setup': 'erpnext.healthcare.setup.setup_healthcare'

View File

@ -43,7 +43,8 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Dosage",
"options": "Prescription Dosage"
"options": "Prescription Dosage",
"reqd": 1
},
{
"fieldname": "period",
@ -51,14 +52,16 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Period",
"options": "Prescription Duration"
"options": "Prescription Duration",
"reqd": 1
},
{
"fieldname": "dosage_form",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Dosage Form",
"options": "Dosage Form"
"options": "Dosage Form",
"reqd": 1
},
{
"fieldname": "column_break_7",
@ -72,7 +75,7 @@
"label": "Comment"
},
{
"depends_on": "use_interval",
"depends_on": "usage_interval",
"fieldname": "interval",
"fieldtype": "Int",
"in_list_view": 1,
@ -80,6 +83,7 @@
},
{
"default": "1",
"depends_on": "usage_interval",
"fieldname": "update_schedule",
"fieldtype": "Check",
"hidden": 1,
@ -99,12 +103,13 @@
"default": "0",
"fieldname": "usage_interval",
"fieldtype": "Check",
"hidden": 1,
"label": "Dosage by Time Interval"
}
],
"istable": 1,
"links": [],
"modified": "2020-02-26 17:02:42.741338",
"modified": "2020-09-30 23:32:09.495288",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Drug Prescription",

View File

@ -0,0 +1,37 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Inpatient Medication Entry', {
refresh: function(frm) {
// Ignore cancellation of doctype on cancel all
frm.ignore_doctypes_on_cancel_all = ['Stock Entry'];
frm.set_query('item_code', () => {
return {
filters: {
is_stock_item: 1
}
};
});
frm.set_query('drug_code', 'medication_orders', () => {
return {
filters: {
is_stock_item: 1
}
};
});
},
get_medication_orders: function(frm) {
frappe.call({
method: 'get_medication_orders',
doc: frm.doc,
freeze: true,
freeze_message: __('Fetching Pending Medication Orders'),
callback: function() {
refresh_field('medication_orders');
}
});
}
});

View File

@ -0,0 +1,203 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2020-09-25 14:13:20.111906",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"company",
"column_break_3",
"posting_date",
"status",
"filters_section",
"item_code",
"assigned_to_practitioner",
"patient",
"practitioner",
"service_unit",
"column_break_11",
"from_date",
"to_date",
"from_time",
"to_time",
"select_medication_orders_section",
"get_medication_orders",
"medication_orders",
"section_break_18",
"update_stock",
"warehouse",
"amended_from"
],
"fields": [
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "HLC-IME-.YYYY.-"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Posting Date",
"reqd": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "\nDraft\nSubmitted\nPending\nIn Process\nCompleted\nCancelled",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "filters_section",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item Code (Drug)",
"options": "Item"
},
{
"depends_on": "update_stock",
"description": "Warehouse from where medication stock should be consumed",
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Medication Warehouse",
"mandatory_depends_on": "update_stock",
"options": "Warehouse"
},
{
"fieldname": "patient",
"fieldtype": "Link",
"label": "Patient",
"options": "Patient"
},
{
"fieldname": "service_unit",
"fieldtype": "Link",
"label": "Healthcare Service Unit",
"options": "Healthcare Service Unit"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date"
},
{
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Inpatient Medication Entry",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "practitioner",
"fieldtype": "Link",
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner"
},
{
"fieldname": "select_medication_orders_section",
"fieldtype": "Section Break",
"label": "Medication Orders"
},
{
"fieldname": "medication_orders",
"fieldtype": "Table",
"label": "Inpatient Medication Orders",
"options": "Inpatient Medication Entry Detail",
"read_only": 1,
"reqd": 1
},
{
"depends_on": "eval:doc.docstatus!==1",
"fieldname": "get_medication_orders",
"fieldtype": "Button",
"label": "Get Pending Medication Orders",
"print_hide": 1
},
{
"fieldname": "assigned_to_practitioner",
"fieldtype": "Link",
"label": "Assigned To",
"options": "User"
},
{
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"label": "Stock Details"
},
{
"default": "1",
"fieldname": "update_stock",
"fieldtype": "Check",
"label": "Update Stock"
},
{
"fieldname": "from_time",
"fieldtype": "Time",
"label": "From Time"
},
{
"fieldname": "to_time",
"fieldtype": "Time",
"label": "To Time"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-09-30 23:40:45.528715",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Entry",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,273 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt, get_link_to_form, getdate, nowtime
from erpnext.stock.utils import get_latest_stock_qty
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_account
class InpatientMedicationEntry(Document):
def validate(self):
self.validate_medication_orders()
def get_medication_orders(self):
self.validate_datetime_filters()
# pull inpatient medication orders based on selected filters
orders = get_pending_medication_orders(self)
if orders:
self.add_mo_to_table(orders)
return self
else:
self.set('medication_orders', [])
frappe.msgprint(_('No pending medication orders found for selected criteria'))
def validate_datetime_filters(self):
if self.from_date and self.to_date:
self.validate_from_to_dates('from_date', 'to_date')
if self.from_date and getdate(self.from_date) > getdate():
frappe.throw(_('From Date cannot be after the current date.'))
if self.to_date and getdate(self.to_date) > getdate():
frappe.throw(_('To Date cannot be after the current date.'))
if self.from_time and self.from_time > nowtime():
frappe.throw(_('From Time cannot be after the current time.'))
if self.to_time and self.to_time > nowtime():
frappe.throw(_('To Time cannot be after the current time.'))
def add_mo_to_table(self, orders):
# Add medication orders in the child table
self.set('medication_orders', [])
for data in orders:
self.append('medication_orders', {
'patient': data.patient,
'patient_name': data.patient_name,
'inpatient_record': data.inpatient_record,
'service_unit': data.service_unit,
'datetime': "%s %s" % (data.date, data.time or "00:00:00"),
'drug_code': data.drug,
'drug_name': data.drug_name,
'dosage': data.dosage,
'dosage_form': data.dosage_form,
'against_imo': data.parent,
'against_imoe': data.name
})
def on_submit(self):
self.validate_medication_orders()
success_msg = ""
if self.update_stock:
stock_entry = self.process_stock()
success_msg += _('Stock Entry {0} created and ').format(
frappe.bold(get_link_to_form('Stock Entry', stock_entry)))
self.update_medication_orders()
success_msg += _('Inpatient Medication Orders updated successfully')
frappe.msgprint(success_msg, title=_('Success'), indicator='green')
def validate_medication_orders(self):
for entry in self.medication_orders:
docstatus, is_completed = frappe.db.get_value('Inpatient Medication Order Entry', entry.against_imoe,
['docstatus', 'is_completed'])
if docstatus == 2:
frappe.throw(_('Row {0}: Cannot create Inpatient Medication Entry against cancelled Inpatient Medication Order {1}').format(
entry.idx, get_link_to_form(entry.against_imo)))
if is_completed:
frappe.throw(_('Row {0}: This Medication Order is already marked as completed').format(
entry.idx))
def on_cancel(self):
self.cancel_stock_entries()
self.update_medication_orders(on_cancel=True)
def process_stock(self):
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
if not allow_negative_stock:
self.check_stock_qty()
return self.make_stock_entry()
def update_medication_orders(self, on_cancel=False):
orders, order_entry_map = self.get_order_entry_map()
# mark completion status
is_completed = 1
if on_cancel:
is_completed = 0
frappe.db.sql("""
UPDATE `tabInpatient Medication Order Entry`
SET is_completed = %(is_completed)s
WHERE name IN %(orders)s
""", {'orders': orders, 'is_completed': is_completed})
# update status and completed orders count
for order, count in order_entry_map.items():
medication_order = frappe.get_doc('Inpatient Medication Order', order)
completed_orders = flt(count)
current_value = frappe.db.get_value('Inpatient Medication Order', order, 'completed_orders')
if on_cancel:
completed_orders = flt(current_value) - flt(count)
else:
completed_orders = flt(current_value) + flt(count)
medication_order.db_set('completed_orders', completed_orders)
medication_order.set_status()
def get_order_entry_map(self):
# for marking order completion status
orders = []
# orders mapped
order_entry_map = dict()
for entry in self.medication_orders:
orders.append(entry.against_imoe)
parent = entry.against_imo
if not order_entry_map.get(parent):
order_entry_map[parent] = 0
order_entry_map[parent] += 1
return orders, order_entry_map
def check_stock_qty(self):
from erpnext.stock.stock_ledger import NegativeStockError
drug_availability = dict()
for d in self.medication_orders:
if not drug_availability.get(d.drug_code):
drug_availability[d.drug_code] = 0
drug_availability[d.drug_code] += flt(d.dosage)
for drug, dosage in drug_availability.items():
available_qty = get_latest_stock_qty(drug, self.warehouse)
# validate qty
if flt(available_qty) < flt(dosage):
frappe.throw(_('Quantity not available for {0} in warehouse {1}').format(
frappe.bold(drug), frappe.bold(self.warehouse))
+ '<br><br>' + _('Available quantity is {0}, you need {1}').format(
frappe.bold(available_qty), frappe.bold(dosage))
+ '<br><br>' + _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.'),
NegativeStockError, title=_('Insufficient Stock'))
def make_stock_entry(self):
stock_entry = frappe.new_doc('Stock Entry')
stock_entry.purpose = 'Material Issue'
stock_entry.set_stock_entry_type()
stock_entry.from_warehouse = self.warehouse
stock_entry.company = self.company
stock_entry.inpatient_medication_entry = self.name
cost_center = frappe.get_cached_value('Company', self.company, 'cost_center')
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company)
for entry in self.medication_orders:
se_child = stock_entry.append('items')
se_child.item_code = entry.drug_code
se_child.item_name = entry.drug_name
se_child.uom = frappe.db.get_value('Item', entry.drug_code, 'stock_uom')
se_child.stock_uom = se_child.uom
se_child.qty = flt(entry.dosage)
# in stock uom
se_child.conversion_factor = 1
se_child.cost_center = cost_center
se_child.expense_account = expense_account
# references
se_child.patient = entry.patient
se_child.inpatient_medication_entry_child = entry.name
stock_entry.submit()
return stock_entry.name
def cancel_stock_entries(self):
stock_entries = frappe.get_all('Stock Entry', {'inpatient_medication_entry': self.name})
for entry in stock_entries:
doc = frappe.get_doc('Stock Entry', entry.name)
doc.cancel()
def get_pending_medication_orders(entry):
filters, values = get_filters(entry)
data = frappe.db.sql("""
SELECT
ip.inpatient_record, ip.patient, ip.patient_name,
entry.name, entry.parent, entry.drug, entry.drug_name,
entry.dosage, entry.dosage_form, entry.date, entry.time, entry.instructions
FROM
`tabInpatient Medication Order` ip
INNER JOIN
`tabInpatient Medication Order Entry` entry
ON
ip.name = entry.parent
WHERE
ip.docstatus = 1 and
ip.company = %(company)s and
entry.is_completed = 0
{0}
ORDER BY
entry.date, entry.time
""".format(filters), values, as_dict=1)
for doc in data:
inpatient_record = doc.inpatient_record
doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record)
if entry.service_unit and doc.service_unit != entry.service_unit:
data.remove(doc)
return data
def get_filters(entry):
filters = ''
values = dict(company=entry.company)
if entry.from_date:
filters += ' and entry.date >= %(from_date)s'
values['from_date'] = entry.from_date
if entry.to_date:
filters += ' and entry.date <= %(to_date)s'
values['to_date'] = entry.to_date
if entry.from_time:
filters += ' and entry.time >= %(from_time)s'
values['from_time'] = entry.from_time
if entry.to_time:
filters += ' and entry.time <= %(to_time)s'
values['to_time'] = entry.to_time
if entry.patient:
filters += ' and ip.patient = %(patient)s'
values['patient'] = entry.patient
if entry.practitioner:
filters += ' and ip.practitioner = %(practitioner)s'
values['practitioner'] = entry.practitioner
if entry.item_code:
filters += ' and entry.drug = %(item_code)s'
values['item_code'] = entry.item_code
if entry.assigned_to_practitioner:
filters += ' and ip._assign LIKE %(assigned_to)s'
values['assigned_to'] = '%' + entry.assigned_to_practitioner + '%'
return filters, values
def get_current_healthcare_service_unit(inpatient_record):
ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
return ip_record.inpatient_occupancies[-1].service_unit

View File

@ -0,0 +1,16 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'against_imoe',
'internal_links': {
'Inpatient Medication Order': ['medication_orders', 'against_imo']
},
'transactions': [
{
'label': _('Reference'),
'items': ['Inpatient Medication Order']
}
]
}

View File

@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import add_days, getdate, now_datetime
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.inpatient_medication_order.test_inpatient_medication_order import create_ipmo, create_ipme
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_account
class TestInpatientMedicationEntry(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabInpatient Record`""")
frappe.db.sql("""delete from `tabInpatient Medication Order`""")
frappe.db.sql("""delete from `tabInpatient Medication Entry`""")
self.patient = create_patient()
# Admit
ip_record = create_inpatient(self.patient)
ip_record.expected_length_of_stay = 0
ip_record.save()
ip_record.reload()
service_unit = get_healthcare_service_unit()
admit_patient(ip_record, service_unit, now_datetime())
self.ip_record = ip_record
def test_filters_for_fetching_pending_mo(self):
ipmo = create_ipmo(self.patient)
ipmo.submit()
ipmo.reload()
date = add_days(getdate(), -1)
filters = frappe._dict(
from_date=date,
to_date=date,
from_time='',
to_time='',
item_code='Dextromethorphan',
patient=self.patient
)
ipme = create_ipme(filters, update_stock=0)
# 3 dosages per day
self.assertEqual(len(ipme.medication_orders), 3)
self.assertEqual(getdate(ipme.medication_orders[0].datetime), date)
def test_ipme_with_stock_update(self):
ipmo = create_ipmo(self.patient)
ipmo.submit()
ipmo.reload()
date = add_days(getdate(), -1)
filters = frappe._dict(
from_date=date,
to_date=date,
from_time='',
to_time='',
item_code='Dextromethorphan',
patient=self.patient
)
make_stock_entry()
ipme = create_ipme(filters, update_stock=1)
ipme.submit()
ipme.reload()
# test order completed
is_order_completed = frappe.db.get_value('Inpatient Medication Order Entry',
ipme.medication_orders[0].against_imoe, 'is_completed')
self.assertEqual(is_order_completed, 1)
# test stock entry
stock_entry = frappe.db.exists('Stock Entry', {'inpatient_medication_entry': ipme.name})
self.assertTrue(stock_entry)
# check references
stock_entry = frappe.get_doc('Stock Entry', stock_entry)
self.assertEqual(stock_entry.items[0].patient, self.patient)
self.assertEqual(stock_entry.items[0].inpatient_medication_entry_child, ipme.medication_orders[0].name)
def tearDown(self):
# cleanup - Discharge
schedule_discharge(frappe.as_json({'patient': self.patient}))
self.ip_record.reload()
mark_invoiced_inpatient_occupancy(self.ip_record)
self.ip_record.reload()
discharge_patient(self.ip_record)
for entry in frappe.get_all('Inpatient Medication Entry'):
doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
doc.cancel()
frappe.db.delete('Stock Entry', {'inpatient_medication_entry': doc.name})
doc.delete()
for entry in frappe.get_all('Inpatient Medication Order'):
doc = frappe.get_doc('Inpatient Medication Order', entry.name)
doc.cancel()
doc.delete()
def make_stock_entry():
frappe.db.set_value('Company', '_Test Company', {
'stock_adjustment_account': 'Stock Adjustment - _TC',
'default_inventory_account': 'Stock In Hand - _TC'
})
stock_entry = frappe.new_doc('Stock Entry')
stock_entry.stock_entry_type = 'Material Receipt'
stock_entry.company = '_Test Company'
stock_entry.to_warehouse = 'Stores - _TC'
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', '_Test Company')
se_child = stock_entry.append('items')
se_child.item_code = 'Dextromethorphan'
se_child.item_name = 'Dextromethorphan'
se_child.uom = 'Nos'
se_child.stock_uom = 'Nos'
se_child.qty = 6
se_child.t_warehouse = 'Stores - _TC'
# in stock uom
se_child.conversion_factor = 1.0
se_child.expense_account = expense_account
stock_entry.submit()

View File

@ -0,0 +1,163 @@
{
"actions": [],
"creation": "2020-09-25 14:56:32.636569",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"patient",
"patient_name",
"inpatient_record",
"column_break_4",
"service_unit",
"datetime",
"medication_details_section",
"drug_code",
"drug_name",
"dosage",
"available_qty",
"dosage_form",
"column_break_10",
"instructions",
"references_section",
"against_imo",
"against_imoe"
],
"fields": [
{
"columns": 2,
"fieldname": "patient",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Patient",
"options": "Patient",
"reqd": 1
},
{
"fetch_from": "patient.patient_name",
"fieldname": "patient_name",
"fieldtype": "Data",
"label": "Patient Name",
"read_only": 1
},
{
"columns": 2,
"fieldname": "drug_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Drug Code",
"options": "Item",
"reqd": 1
},
{
"fetch_from": "drug_code.item_name",
"fieldname": "drug_name",
"fieldtype": "Data",
"label": "Drug Name",
"read_only": 1
},
{
"columns": 1,
"fieldname": "dosage",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Dosage",
"reqd": 1
},
{
"fieldname": "dosage_form",
"fieldtype": "Link",
"label": "Dosage Form",
"options": "Dosage Form"
},
{
"fetch_from": "patient.inpatient_record",
"fieldname": "inpatient_record",
"fieldtype": "Link",
"label": "Inpatient Record",
"options": "Inpatient Record",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "references_section",
"fieldtype": "Section Break",
"label": "References"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "medication_details_section",
"fieldtype": "Section Break",
"label": "Medication Details"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"columns": 3,
"fieldname": "datetime",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Datetime",
"reqd": 1
},
{
"fieldname": "instructions",
"fieldtype": "Small Text",
"label": "Instructions"
},
{
"columns": 2,
"fieldname": "service_unit",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Service Unit",
"options": "Healthcare Service Unit",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "against_imo",
"fieldtype": "Link",
"label": "Against Inpatient Medication Order",
"no_copy": 1,
"options": "Inpatient Medication Order",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "against_imoe",
"fieldtype": "Data",
"label": "Against Inpatient Medication Order Entry",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "available_qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Available Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-09-30 14:48:23.648223",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Entry Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class InpatientMedicationEntryDetail(Document):
pass

View File

@ -0,0 +1,106 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Inpatient Medication Order', {
refresh: function(frm) {
if (frm.doc.docstatus === 1) {
frm.trigger("show_progress");
}
frm.events.show_medication_order_button(frm);
frm.set_query('patient', () => {
return {
filters: {
'inpatient_record': ['!=', '']
}
};
});
},
show_medication_order_button: function(frm) {
frm.fields_dict['medication_orders'].grid.wrapper.find('.grid-add-row').hide();
frm.fields_dict['medication_orders'].grid.add_custom_button(__('Add Medication Orders'), () => {
let d = new frappe.ui.Dialog({
title: __('Add Medication Orders'),
fields: [
{
fieldname: 'drug_code',
label: __('Drug'),
fieldtype: 'Link',
options: 'Item',
reqd: 1,
"get_query": function () {
return {
filters: {'is_stock_item': 1}
};
}
},
{
fieldname: 'dosage',
label: __('Dosage'),
fieldtype: 'Link',
options: 'Prescription Dosage',
reqd: 1
},
{
fieldname: 'period',
label: __('Period'),
fieldtype: 'Link',
options: 'Prescription Duration',
reqd: 1
},
{
fieldname: 'dosage_form',
label: __('Dosage Form'),
fieldtype: 'Link',
options: 'Dosage Form',
reqd: 1
}
],
primary_action_label: __('Add'),
primary_action: () => {
let values = d.get_values();
if (values) {
frm.call({
doc: frm.doc,
method: 'add_order_entries',
args: {
order: values
},
freeze: true,
freeze_message: __('Adding Order Entries'),
callback: function() {
frm.refresh_field('medication_orders');
}
});
}
},
});
d.show();
});
},
show_progress: function(frm) {
let bars = [];
let message = '';
// completed sessions
let title = __('{0} medication orders completed', [frm.doc.completed_orders]);
if (frm.doc.completed_orders === 1) {
title = __('{0} medication order completed', [frm.doc.completed_orders]);
}
title += __(' out of {0}', [frm.doc.total_orders]);
bars.push({
'title': title,
'width': (frm.doc.completed_orders / frm.doc.total_orders * 100) + '%',
'progress_class': 'progress-bar-success'
});
if (bars[0].width == '0%') {
bars[0].width = '0.5%';
}
message = title;
frm.dashboard.add_progress(__('Status'), bars, message);
}
});

View File

@ -0,0 +1,196 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2020-09-14 18:33:56.715736",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"patient_details_section",
"naming_series",
"patient_encounter",
"patient",
"patient_name",
"patient_age",
"inpatient_record",
"column_break_6",
"company",
"status",
"practitioner",
"start_date",
"end_date",
"medication_orders_section",
"medication_orders",
"section_break_16",
"total_orders",
"column_break_18",
"completed_orders",
"amended_from"
],
"fields": [
{
"fieldname": "patient_details_section",
"fieldtype": "Section Break",
"label": "Patient Details"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "HLC-IMO-.YYYY.-"
},
{
"fieldname": "patient_encounter",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Patient Encounter",
"options": "Patient Encounter"
},
{
"fetch_from": "patient_encounter.patient",
"fieldname": "patient",
"fieldtype": "Link",
"label": "Patient",
"options": "Patient",
"read_only_depends_on": "patient_encounter",
"reqd": 1
},
{
"fetch_from": "patient.patient_name",
"fieldname": "patient_name",
"fieldtype": "Data",
"label": "Patient Name",
"read_only": 1
},
{
"fieldname": "patient_age",
"fieldtype": "Data",
"label": "Patient Age",
"read_only": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fetch_from": "patient.inpatient_record",
"fieldname": "inpatient_record",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Inpatient Record",
"options": "Inpatient Record",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "patient_encounter.practitioner",
"fieldname": "practitioner",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner",
"read_only_depends_on": "patient_encounter"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date",
"reqd": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date",
"read_only": 1
},
{
"depends_on": "eval: doc.patient && doc.start_date",
"fieldname": "medication_orders_section",
"fieldtype": "Section Break",
"label": "Medication Orders"
},
{
"fieldname": "medication_orders",
"fieldtype": "Table",
"label": "Medication Orders",
"options": "Inpatient Medication Order Entry"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Inpatient Medication Order",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "\nDraft\nSubmitted\nPending\nIn Process\nCompleted\nCancelled",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "section_break_16",
"fieldtype": "Section Break",
"label": "Other Details"
},
{
"fieldname": "total_orders",
"fieldtype": "Float",
"label": "Total Orders",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fieldname": "completed_orders",
"fieldtype": "Float",
"label": "Completed Orders",
"no_copy": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-09-30 21:53:27.128591",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Order",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "patient_encounter, patient",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "patient",
"track_changes": 1
}

View File

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr
from erpnext.healthcare.doctype.patient_encounter.patient_encounter import get_prescription_dates
class InpatientMedicationOrder(Document):
def validate(self):
self.validate_inpatient()
self.validate_duplicate()
self.set_total_orders()
self.set_status()
def on_submit(self):
self.validate_inpatient()
self.set_status()
def on_cancel(self):
self.set_status()
def validate_inpatient(self):
if not self.inpatient_record:
frappe.throw(_('No Inpatient Record found against patient {0}').format(self.patient))
def validate_duplicate(self):
existing_mo = frappe.db.exists('Inpatient Medication Order', {
'patient_encounter': self.patient_encounter,
'docstatus': ('!=', 2),
'name': ('!=', self.name)
})
if existing_mo:
frappe.throw(_('An Inpatient Medication Order {0} against Patient Encounter {1} already exists.').format(
existing_mo, self.patient_encounter), frappe.DuplicateEntryError)
def set_total_orders(self):
self.db_set('total_orders', len(self.medication_orders))
def set_status(self):
status = {
"0": "Draft",
"1": "Submitted",
"2": "Cancelled"
}[cstr(self.docstatus or 0)]
if self.docstatus == 1:
if not self.completed_orders:
status = 'Pending'
elif self.completed_orders < self.total_orders:
status = 'In Process'
else:
status = 'Completed'
self.db_set('status', status)
def add_order_entries(self, order):
if order.get('drug_code'):
dosage = frappe.get_doc('Prescription Dosage', order.get('dosage'))
dates = get_prescription_dates(order.get('period'), self.start_date)
for date in dates:
for dose in dosage.dosage_strength:
entry = self.append('medication_orders')
entry.drug = order.get('drug_code')
entry.drug_name = frappe.db.get_value('Item', order.get('drug_code'), 'item_name')
entry.dosage = dose.strength
entry.dosage_form = order.get('dosage_form')
entry.date = date
entry.time = dose.strength_time
self.end_date = dates[-1]
return

View File

@ -0,0 +1,16 @@
frappe.listview_settings['Inpatient Medication Order'] = {
add_fields: ["status"],
filters: [["status", "!=", "Cancelled"]],
get_indicator: function(doc) {
if (doc.status === "Pending") {
return [__("Pending"), "orange", "status,=,Pending"];
} else if (doc.status === "In Process") {
return [__("In Process"), "blue", "status,=,In Process"];
} else if (doc.status === "Completed") {
return [__("Completed"), "green", "status,=,Completed"];
}
}
};

View File

@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import add_days, getdate, now_datetime
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
class TestInpatientMedicationOrder(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabInpatient Record`""")
self.patient = create_patient()
# Admit
ip_record = create_inpatient(self.patient)
ip_record.expected_length_of_stay = 0
ip_record.save()
ip_record.reload()
service_unit = get_healthcare_service_unit()
admit_patient(ip_record, service_unit, now_datetime())
self.ip_record = ip_record
def test_order_creation(self):
ipmo = create_ipmo(self.patient)
ipmo.submit()
ipmo.reload()
# 3 dosages per day for 2 days
self.assertEqual(len(ipmo.medication_orders), 6)
self.assertEqual(ipmo.medication_orders[0].date, add_days(getdate(), -1))
prescription_dosage = frappe.get_doc('Prescription Dosage', '1-1-1')
for i in range(len(prescription_dosage.dosage_strength)):
self.assertEqual(ipmo.medication_orders[i].time, prescription_dosage.dosage_strength[i].strength_time)
self.assertEqual(ipmo.medication_orders[3].date, getdate())
def test_inpatient_validation(self):
# Discharge
schedule_discharge(frappe.as_json({'patient': self.patient}))
self.ip_record.reload()
mark_invoiced_inpatient_occupancy(self.ip_record)
self.ip_record.reload()
discharge_patient(self.ip_record)
ipmo = create_ipmo(self.patient)
# inpatient validation
self.assertRaises(frappe.ValidationError, ipmo.insert)
def test_status(self):
ipmo = create_ipmo(self.patient)
ipmo.submit()
ipmo.reload()
self.assertEqual(ipmo.status, 'Pending')
filters = frappe._dict(from_date=add_days(getdate(), -1), to_date=add_days(getdate(), -1), from_time='', to_time='')
ipme = create_ipme(filters)
ipme.submit()
ipmo.reload()
self.assertEqual(ipmo.status, 'In Process')
filters = frappe._dict(from_date=getdate(), to_date=getdate(), from_time='', to_time='')
ipme = create_ipme(filters)
ipme.submit()
ipmo.reload()
self.assertEqual(ipmo.status, 'Completed')
def tearDown(self):
if frappe.db.get_value('Patient', self.patient, 'inpatient_record'):
# cleanup - Discharge
schedule_discharge(frappe.as_json({'patient': self.patient}))
self.ip_record.reload()
mark_invoiced_inpatient_occupancy(self.ip_record)
self.ip_record.reload()
discharge_patient(self.ip_record)
for entry in frappe.get_all('Inpatient Medication Entry'):
doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
doc.cancel()
doc.delete()
for entry in frappe.get_all('Inpatient Medication Order'):
doc = frappe.get_doc('Inpatient Medication Order', entry.name)
doc.cancel()
doc.delete()
def create_dosage_form():
if not frappe.db.exists('Dosage Form', 'Tablet'):
frappe.get_doc({
'doctype': 'Dosage Form',
'dosage_form': 'Tablet'
}).insert()
def create_drug(item=None):
if not item:
item = 'Dextromethorphan'
drug = frappe.db.exists('Item', {'item_code': 'Dextromethorphan'})
if not drug:
drug = frappe.get_doc({
'doctype': 'Item',
'item_code': 'Dextromethorphan',
'item_name': 'Dextromethorphan',
'item_group': 'Products',
'stock_uom': 'Nos',
'is_stock_item': 1,
'valuation_rate': 50,
'opening_stock': 20
}).insert()
def get_orders():
create_dosage_form()
create_drug()
return {
'drug_code': 'Dextromethorphan',
'drug_name': 'Dextromethorphan',
'dosage': '1-1-1',
'dosage_form': 'Tablet',
'period': '2 Day'
}
def create_ipmo(patient):
orders = get_orders()
ipmo = frappe.new_doc('Inpatient Medication Order')
ipmo.patient = patient
ipmo.company = '_Test Company'
ipmo.start_date = add_days(getdate(), -1)
ipmo.add_order_entries(orders)
return ipmo
def create_ipme(filters, update_stock=0):
ipme = frappe.new_doc('Inpatient Medication Entry')
ipme.company = '_Test Company'
ipme.posting_date = getdate()
ipme.update_stock = update_stock
if update_stock:
ipme.warehouse = 'Stores - _TC'
for key, value in filters.items():
ipme.set(key, value)
ipme = ipme.get_medication_orders()
return ipme

View File

@ -0,0 +1,94 @@
{
"actions": [],
"creation": "2020-09-14 21:51:30.259164",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"drug",
"drug_name",
"dosage",
"dosage_form",
"instructions",
"column_break_4",
"date",
"time",
"is_completed"
],
"fields": [
{
"fieldname": "drug",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Drug",
"options": "Item",
"reqd": 1
},
{
"fetch_from": "drug.item_name",
"fieldname": "drug_name",
"fieldtype": "Data",
"label": "Drug Name",
"read_only": 1
},
{
"fieldname": "dosage",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Dosage",
"reqd": 1
},
{
"fieldname": "dosage_form",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Dosage Form",
"options": "Dosage Form",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"reqd": 1
},
{
"fieldname": "time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "Time",
"reqd": 1
},
{
"default": "0",
"fieldname": "is_completed",
"fieldtype": "Check",
"label": "Is Order Completed",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "instructions",
"fieldtype": "Small Text",
"label": "Instructions"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-09-30 14:03:26.755925",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Order Entry",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class InpatientMedicationOrderEntry(Document):
pass

View File

@ -83,6 +83,7 @@ def get_healthcare_service_unit():
if not service_unit:
service_unit = frappe.new_doc("Healthcare Service Unit")
service_unit.healthcare_service_unit_name = "Test Service Unit Ip Occupancy"
service_unit.company = "_Test Company"
service_unit.service_unit_type = get_service_unit_type()
service_unit.inpatient_occupancy = 1
service_unit.occupancy_status = "Vacant"

View File

@ -58,6 +58,14 @@ frappe.ui.form.on('Patient Encounter', {
create_procedure(frm);
},'Create');
if (frm.doc.drug_prescription && frm.doc.inpatient_record && frm.doc.inpatient_status === "Admitted") {
frm.add_custom_button(__('Inpatient Medication Order'), function() {
frappe.model.open_mapped_doc({
method: 'erpnext.healthcare.doctype.patient_encounter.patient_encounter.make_ip_medication_order',
frm: frm
});
}, 'Create');
}
}
frm.set_query('patient', function() {

View File

@ -6,8 +6,9 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr
from frappe.utils import cstr, getdate, add_days
from frappe import _
from frappe.model.mapper import get_mapped_doc
class PatientEncounter(Document):
def validate(self):
@ -22,20 +23,69 @@ class PatientEncounter(Document):
insert_encounter_to_medical_record(self)
def on_submit(self):
update_encounter_medical_record(self)
if self.therapies:
create_therapy_plan(self)
def on_cancel(self):
if self.appointment:
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open')
delete_medical_record(self)
def on_submit(self):
create_therapy_plan(self)
if self.inpatient_record and self.drug_prescription:
delete_ip_medication_order(self)
delete_medical_record(self)
def set_title(self):
self.title = _('{0} with {1}').format(self.patient_name or self.patient,
self.practitioner_name or self.practitioner)[:100]
@frappe.whitelist()
def make_ip_medication_order(source_name, target_doc=None):
def set_missing_values(source, target):
target.start_date = source.encounter_date
for entry in source.drug_prescription:
if entry.drug_code:
dosage = frappe.get_doc('Prescription Dosage', entry.dosage)
dates = get_prescription_dates(entry.period, target.start_date)
for date in dates:
for dose in dosage.dosage_strength:
order = target.append('medication_orders')
order.drug = entry.drug_code
order.drug_name = entry.drug_name
order.dosage = dose.strength
order.instructions = entry.comment
order.dosage_form = entry.dosage_form
order.date = date
order.time = dose.strength_time
target.end_date = dates[-1]
doc = get_mapped_doc('Patient Encounter', source_name, {
'Patient Encounter': {
'doctype': 'Inpatient Medication Order',
'field_map': {
'name': 'patient_encounter',
'patient': 'patient',
'patient_name': 'patient_name',
'patient_age': 'patient_age',
'inpatient_record': 'inpatient_record',
'practitioner': 'practitioner',
'start_date': 'encounter_date'
},
}
}, target_doc, set_missing_values)
return doc
def get_prescription_dates(period, start_date):
prescription_duration = frappe.get_doc('Prescription Duration', period)
days = prescription_duration.get_days()
dates = [start_date]
for i in range(1, days):
dates.append(add_days(getdate(start_date), i))
return dates
def create_therapy_plan(encounter):
if len(encounter.therapies):
doc = frappe.new_doc('Therapy Plan')
@ -51,6 +101,7 @@ def create_therapy_plan(encounter):
encounter.db_set('therapy_plan', doc.name)
frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True)
def insert_encounter_to_medical_record(doc):
subject = set_subject_field(doc)
medical_record = frappe.new_doc('Patient Medical Record')
@ -63,6 +114,7 @@ def insert_encounter_to_medical_record(doc):
medical_record.reference_owner = doc.owner
medical_record.save(ignore_permissions=True)
def update_encounter_medical_record(encounter):
medical_record_id = frappe.db.exists('Patient Medical Record', {'reference_name': encounter.name})
@ -72,8 +124,17 @@ def update_encounter_medical_record(encounter):
else:
insert_encounter_to_medical_record(encounter)
def delete_medical_record(encounter):
frappe.delete_doc_if_exists('Patient Medical Record', 'reference_name', encounter.name)
record = frappe.db.exists('Patient Medical Record', {'reference_name', encounter.name})
if record:
frappe.delete_doc('Patient Medical Record', record, force=1)
def delete_ip_medication_order(encounter):
record = frappe.db.exists('Inpatient Medication Order', {'patient_encounter': encounter.name})
if record:
frappe.delete_doc('Inpatient Medication Order', record, force=1)
def set_subject_field(encounter):
subject = frappe.bold(_('Healthcare Practitioner: ')) + encounter.practitioner + '<br>'

View File

@ -5,12 +5,18 @@ def get_data():
return {
'fieldname': 'encounter',
'non_standard_fieldnames': {
'Patient Medical Record': 'reference_name'
'Patient Medical Record': 'reference_name',
'Inpatient Medication Order': 'patient_encounter'
},
'transactions': [
{
'label': _('Records'),
'items': ['Vital Signs', 'Patient Medical Record']
},
]
{
'label': _('Orders'),
'items': ['Inpatient Medication Order']
}
],
'disable_create_buttons': ['Inpatient Medication Order']
}

View File

@ -282,7 +282,8 @@ doc_events = {
# to maintain data integrity we exempted payment entry. it will un-link when sales invoice get cancelled.
# if payment entry not in auto cancel exempted doctypes it will cancel payment entry.
auto_cancel_exempted_doctypes= [
"Payment Entry"
"Payment Entry",
"Inpatient Medication Entry"
]
scheduler_events = {

View File

@ -731,3 +731,4 @@ erpnext.patches.v13_0.change_default_pos_print_format
erpnext.patches.v13_0.set_youtube_video_id
erpnext.patches.v13_0.print_uom_after_quantity_patch
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail

View File

@ -0,0 +1,10 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.domains.healthcare import data
def execute():
if 'Healthcare' not in frappe.get_active_domains():
return
if data['custom_fields']:
create_custom_fields(data['custom_fields'])