From 4887b6994645f4398eb8f67adf061c1a27b9add6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 23 Oct 2020 20:33:30 +0530 Subject: [PATCH] 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 --- erpnext/domains/healthcare.py | 16 + .../drug_prescription/drug_prescription.json | 15 +- .../inpatient_medication_entry/__init__.py | 0 .../inpatient_medication_entry.js | 37 +++ .../inpatient_medication_entry.json | 203 +++++++++++++ .../inpatient_medication_entry.py | 273 ++++++++++++++++++ .../inpatient_medication_entry_dashboard.py | 16 + .../test_inpatient_medication_entry.py | 125 ++++++++ .../__init__.py | 0 .../inpatient_medication_entry_detail.json | 163 +++++++++++ .../inpatient_medication_entry_detail.py | 10 + .../inpatient_medication_order/__init__.py | 0 .../inpatient_medication_order.js | 106 +++++++ .../inpatient_medication_order.json | 196 +++++++++++++ .../inpatient_medication_order.py | 74 +++++ .../inpatient_medication_order_list.js | 16 + .../test_inpatient_medication_order.py | 150 ++++++++++ .../__init__.py | 0 .../inpatient_medication_order_entry.json | 94 ++++++ .../inpatient_medication_order_entry.py | 10 + .../inpatient_record/test_inpatient_record.py | 1 + .../patient_encounter/patient_encounter.js | 8 + .../patient_encounter/patient_encounter.py | 73 ++++- .../patient_encounter_dashboard.py | 10 +- erpnext/hooks.py | 3 +- erpnext/patches.txt | 1 + ...are_custom_fields_in_stock_entry_detail.py | 10 + 27 files changed, 1596 insertions(+), 14 deletions(-) create mode 100644 erpnext/healthcare/doctype/inpatient_medication_entry/__init__.py create mode 100644 erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js create mode 100644 erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json create mode 100644 erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py create mode 100644 erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry_dashboard.py create mode 100644 erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py create mode 100644 erpnext/healthcare/doctype/inpatient_medication_entry_detail/__init__.py create mode 100644 erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.json create mode 100644 erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.py create mode 100644 erpnext/healthcare/doctype/inpatient_medication_order/__init__.py create mode 100644 erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.js create mode 100644 erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.json create mode 100644 erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py create mode 100644 erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order_list.js create mode 100644 erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py create mode 100644 erpnext/healthcare/doctype/inpatient_medication_order_entry/__init__.py create mode 100644 erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.json create mode 100644 erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.py create mode 100644 erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py diff --git a/erpnext/domains/healthcare.py b/erpnext/domains/healthcare.py index 8bd4c76290..bbeb2c66bc 100644 --- a/erpnext/domains/healthcare.py +++ b/erpnext/domains/healthcare.py @@ -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' diff --git a/erpnext/healthcare/doctype/drug_prescription/drug_prescription.json b/erpnext/healthcare/doctype/drug_prescription/drug_prescription.json index 5e4d59cacf..d91e6bf9dc 100644 --- a/erpnext/healthcare/doctype/drug_prescription/drug_prescription.json +++ b/erpnext/healthcare/doctype/drug_prescription/drug_prescription.json @@ -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", diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/__init__.py b/erpnext/healthcare/doctype/inpatient_medication_entry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js new file mode 100644 index 0000000000..b953b8adff --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js @@ -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'); + } + }); + } +}); diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json new file mode 100644 index 0000000000..5d80251b71 --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json @@ -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 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py new file mode 100644 index 0000000000..2385893109 --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py @@ -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)) + + '

' + _('Available quantity is {0}, you need {1}').format( + frappe.bold(available_qty), frappe.bold(dosage)) + + '

' + _('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 \ No newline at end of file diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry_dashboard.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry_dashboard.py new file mode 100644 index 0000000000..a4bec45596 --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry_dashboard.py @@ -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'] + } + ] + } diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py new file mode 100644 index 0000000000..2f1bb6b56f --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py @@ -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() \ No newline at end of file diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry_detail/__init__.py b/erpnext/healthcare/doctype/inpatient_medication_entry_detail/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.json b/erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.json new file mode 100644 index 0000000000..e3d7212169 --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.json @@ -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 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.py b/erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.py new file mode 100644 index 0000000000..644898d9ed --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_entry_detail/inpatient_medication_entry_detail.py @@ -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 diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/__init__.py b/erpnext/healthcare/doctype/inpatient_medication_order/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.js b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.js new file mode 100644 index 0000000000..c51f3cf882 --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.js @@ -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); + } +}); diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.json b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.json new file mode 100644 index 0000000000..e31d2e3e36 --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.json @@ -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 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py new file mode 100644 index 0000000000..33cbbec812 --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py @@ -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 diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order_list.js b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order_list.js new file mode 100644 index 0000000000..1c318768ea --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order_list.js @@ -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"]; + + } + } +}; diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py new file mode 100644 index 0000000000..a21caca8ff --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py @@ -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 + diff --git a/erpnext/healthcare/doctype/inpatient_medication_order_entry/__init__.py b/erpnext/healthcare/doctype/inpatient_medication_order_entry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.json b/erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.json new file mode 100644 index 0000000000..72999a908e --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.json @@ -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 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.py b/erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.py new file mode 100644 index 0000000000..ebfe366346 --- /dev/null +++ b/erpnext/healthcare/doctype/inpatient_medication_order_entry/inpatient_medication_order_entry.py @@ -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 diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index 2bef5fb5bd..70706adb2e 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -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" diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js index 6353d19ef1..e960f0a9c4 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js @@ -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() { diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py index 262fc4650a..87f42491fc 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py @@ -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 + '
' diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter_dashboard.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter_dashboard.py index b08b172bba..39e54f5b35 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter_dashboard.py +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter_dashboard.py @@ -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'] } diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 9bd31050c4..dbb6c0d92e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -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 = { diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5b45c22123..8b34eaa0a8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -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 diff --git a/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py b/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py new file mode 100644 index 0000000000..585e540626 --- /dev/null +++ b/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py @@ -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']) \ No newline at end of file