# -*- coding: utf-8 -*- # Copyright (c) 2018, earthians and contributors # For license information, please see license.txt from __future__ import unicode_literals import math import frappe import json from frappe import _ from frappe.utils.formatters import format_value from frappe.utils import time_diff_in_hours, rounded from six import string_types from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple @frappe.whitelist() def get_healthcare_services_to_invoice(patient, company): patient = frappe.get_doc('Patient', patient) items_to_invoice = [] if patient: validate_customer_created(patient) # Customer validated, build a list of billable services items_to_invoice += get_appointments_to_invoice(patient, company) items_to_invoice += get_encounters_to_invoice(patient, company) items_to_invoice += get_lab_tests_to_invoice(patient, company) items_to_invoice += get_clinical_procedures_to_invoice(patient, company) items_to_invoice += get_inpatient_services_to_invoice(patient, company) items_to_invoice += get_therapy_plans_to_invoice(patient, company) items_to_invoice += get_therapy_sessions_to_invoice(patient, company) return items_to_invoice def validate_customer_created(patient): if not frappe.db.get_value('Patient', patient.name, 'customer'): msg = _("Please set a Customer linked to the Patient") msg += " <b><a href='/app/Form/Patient/{0}'>{0}</a></b>".format(patient.name) frappe.throw(msg, title=_('Customer Not Found')) def get_appointments_to_invoice(patient, company): appointments_to_invoice = [] patient_appointments = frappe.get_list( 'Patient Appointment', fields = '*', filters = {'patient': patient.name, 'company': company, 'invoiced': 0, 'status': ['not in', 'Cancelled']}, order_by = 'appointment_date' ) for appointment in patient_appointments: # Procedure Appointments if appointment.procedure_template: if frappe.db.get_value('Clinical Procedure Template', appointment.procedure_template, 'is_billable'): appointments_to_invoice.append({ 'reference_type': 'Patient Appointment', 'reference_name': appointment.name, 'service': appointment.procedure_template }) # Consultation Appointments, should check fee validity else: if frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups') and \ frappe.db.exists('Fee Validity Reference', {'appointment': appointment.name}): continue # Skip invoicing, fee validty present practitioner_charge = 0 income_account = None service_item = None if appointment.practitioner: details = get_service_item_and_practitioner_charge(appointment) service_item = details.get('service_item') practitioner_charge = details.get('practitioner_charge') income_account = get_income_account(appointment.practitioner, appointment.company) appointments_to_invoice.append({ 'reference_type': 'Patient Appointment', 'reference_name': appointment.name, 'service': service_item, 'rate': practitioner_charge, 'income_account': income_account }) return appointments_to_invoice def get_encounters_to_invoice(patient, company): if not isinstance(patient, str): patient = patient.name encounters_to_invoice = [] encounters = frappe.get_list( 'Patient Encounter', fields=['*'], filters={'patient': patient, 'company': company, 'invoiced': False, 'docstatus': 1} ) if encounters: for encounter in encounters: if not encounter.appointment: practitioner_charge = 0 income_account = None service_item = None if encounter.practitioner: if encounter.inpatient_record and \ frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'): continue details = get_service_item_and_practitioner_charge(encounter) service_item = details.get('service_item') practitioner_charge = details.get('practitioner_charge') income_account = get_income_account(encounter.practitioner, encounter.company) encounters_to_invoice.append({ 'reference_type': 'Patient Encounter', 'reference_name': encounter.name, 'service': service_item, 'rate': practitioner_charge, 'income_account': income_account }) return encounters_to_invoice def get_lab_tests_to_invoice(patient, company): lab_tests_to_invoice = [] lab_tests = frappe.get_list( 'Lab Test', fields=['name', 'template'], filters={'patient': patient.name, 'company': company, 'invoiced': False, 'docstatus': 1} ) for lab_test in lab_tests: item, is_billable = frappe.get_cached_value('Lab Test Template', lab_test.template, ['item', 'is_billable']) if is_billable: lab_tests_to_invoice.append({ 'reference_type': 'Lab Test', 'reference_name': lab_test.name, 'service': item }) lab_prescriptions = frappe.db.sql( ''' SELECT lp.name, lp.lab_test_code FROM `tabPatient Encounter` et, `tabLab Prescription` lp WHERE et.patient=%s and lp.parent=et.name and lp.lab_test_created=0 and lp.invoiced=0 ''', (patient.name), as_dict=1) for prescription in lab_prescriptions: item, is_billable = frappe.get_cached_value('Lab Test Template', prescription.lab_test_code, ['item', 'is_billable']) if prescription.lab_test_code and is_billable: lab_tests_to_invoice.append({ 'reference_type': 'Lab Prescription', 'reference_name': prescription.name, 'service': item }) return lab_tests_to_invoice def get_clinical_procedures_to_invoice(patient, company): clinical_procedures_to_invoice = [] procedures = frappe.get_list( 'Clinical Procedure', fields='*', filters={'patient': patient.name, 'company': company, 'invoiced': False} ) for procedure in procedures: if not procedure.appointment: item, is_billable = frappe.get_cached_value('Clinical Procedure Template', procedure.procedure_template, ['item', 'is_billable']) if procedure.procedure_template and is_billable: clinical_procedures_to_invoice.append({ 'reference_type': 'Clinical Procedure', 'reference_name': procedure.name, 'service': item }) # consumables if procedure.invoice_separately_as_consumables and procedure.consume_stock \ and procedure.status == 'Completed' and not procedure.consumption_invoiced: service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') if not service_item: msg = _('Please Configure Clinical Procedure Consumable Item in ') msg += '''<b><a href='/app/Form/Healthcare Settings'>Healthcare Settings</a></b>''' frappe.throw(msg, title=_('Missing Configuration')) clinical_procedures_to_invoice.append({ 'reference_type': 'Clinical Procedure', 'reference_name': procedure.name, 'service': service_item, 'rate': procedure.consumable_total_amount, 'description': procedure.consumption_details }) procedure_prescriptions = frappe.db.sql( ''' SELECT pp.name, pp.procedure FROM `tabPatient Encounter` et, `tabProcedure Prescription` pp WHERE et.patient=%s and pp.parent=et.name and pp.procedure_created=0 and pp.invoiced=0 and pp.appointment_booked=0 ''', (patient.name), as_dict=1) for prescription in procedure_prescriptions: item, is_billable = frappe.get_cached_value('Clinical Procedure Template', prescription.procedure, ['item', 'is_billable']) if is_billable: clinical_procedures_to_invoice.append({ 'reference_type': 'Procedure Prescription', 'reference_name': prescription.name, 'service': item }) return clinical_procedures_to_invoice def get_inpatient_services_to_invoice(patient, company): services_to_invoice = [] inpatient_services = frappe.db.sql( ''' SELECT io.* FROM `tabInpatient Record` ip, `tabInpatient Occupancy` io WHERE ip.patient=%s and ip.company=%s and io.parent=ip.name and io.left=1 and io.invoiced=0 ''', (patient.name, company), as_dict=1) for inpatient_occupancy in inpatient_services: service_unit_type = frappe.db.get_value('Healthcare Service Unit', inpatient_occupancy.service_unit, 'service_unit_type') service_unit_type = frappe.get_cached_doc('Healthcare Service Unit Type', service_unit_type) if service_unit_type and service_unit_type.is_billable: hours_occupied = time_diff_in_hours(inpatient_occupancy.check_out, inpatient_occupancy.check_in) qty = 0.5 if hours_occupied > 0: actual_qty = hours_occupied / service_unit_type.no_of_hours floor = math.floor(actual_qty) decimal_part = actual_qty - floor if decimal_part > 0.5: qty = rounded(floor + 1, 1) elif decimal_part < 0.5 and decimal_part > 0: qty = rounded(floor + 0.5, 1) if qty <= 0: qty = 0.5 services_to_invoice.append({ 'reference_type': 'Inpatient Occupancy', 'reference_name': inpatient_occupancy.name, 'service': service_unit_type.item, 'qty': qty }) return services_to_invoice def get_therapy_plans_to_invoice(patient, company): therapy_plans_to_invoice = [] therapy_plans = frappe.get_list( 'Therapy Plan', fields=['therapy_plan_template', 'name'], filters={ 'patient': patient.name, 'invoiced': 0, 'company': company, 'therapy_plan_template': ('!=', '') } ) for plan in therapy_plans: therapy_plans_to_invoice.append({ 'reference_type': 'Therapy Plan', 'reference_name': plan.name, 'service': frappe.db.get_value('Therapy Plan Template', plan.therapy_plan_template, 'linked_item') }) return therapy_plans_to_invoice def get_therapy_sessions_to_invoice(patient, company): therapy_sessions_to_invoice = [] therapy_plans = frappe.db.get_all('Therapy Plan', {'therapy_plan_template': ('!=', '')}) therapy_plans_created_from_template = [] for entry in therapy_plans: therapy_plans_created_from_template.append(entry.name) therapy_sessions = frappe.get_list( 'Therapy Session', fields='*', filters={ 'patient': patient.name, 'invoiced': 0, 'company': company, 'therapy_plan': ('not in', therapy_plans_created_from_template) } ) for therapy in therapy_sessions: if not therapy.appointment: if therapy.therapy_type and frappe.db.get_value('Therapy Type', therapy.therapy_type, 'is_billable'): therapy_sessions_to_invoice.append({ 'reference_type': 'Therapy Session', 'reference_name': therapy.name, 'service': frappe.db.get_value('Therapy Type', therapy.therapy_type, 'item') }) return therapy_sessions_to_invoice @frappe.whitelist() def get_service_item_and_practitioner_charge(doc): if isinstance(doc, string_types): doc = json.loads(doc) doc = frappe.get_doc(doc) service_item = None practitioner_charge = None department = doc.medical_department if doc.doctype == 'Patient Encounter' else doc.department is_inpatient = doc.inpatient_record if doc.get('appointment_type'): service_item, practitioner_charge = get_appointment_type_service_item(doc.appointment_type, department, is_inpatient) if not service_item and not practitioner_charge: service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient) if not service_item: service_item = get_healthcare_service_item(is_inpatient) if not service_item: throw_config_service_item(is_inpatient) if not practitioner_charge: throw_config_practitioner_charge(is_inpatient, doc.practitioner) return {'service_item': service_item, 'practitioner_charge': practitioner_charge} def get_appointment_type_service_item(appointment_type, department, is_inpatient): from erpnext.healthcare.doctype.appointment_type.appointment_type import get_service_item_based_on_department item_list = get_service_item_based_on_department(appointment_type, department) service_item = None practitioner_charge = None if item_list: if is_inpatient: service_item = item_list.get('inpatient_visit_charge_item') practitioner_charge = item_list.get('inpatient_visit_charge') else: service_item = item_list.get('op_consulting_charge_item') practitioner_charge = item_list.get('op_consulting_charge') return service_item, practitioner_charge def throw_config_service_item(is_inpatient): service_item_label = _('Out Patient Consulting Charge Item') if is_inpatient: service_item_label = _('Inpatient Visit Charge Item') msg = _(('Please Configure {0} in ').format(service_item_label) \ + '''<b><a href='/app/Form/Healthcare Settings'>Healthcare Settings</a></b>''') frappe.throw(msg, title=_('Missing Configuration')) def throw_config_practitioner_charge(is_inpatient, practitioner): charge_name = _('OP Consulting Charge') if is_inpatient: charge_name = _('Inpatient Visit Charge') msg = _(('Please Configure {0} for Healthcare Practitioner').format(charge_name) \ + ''' <b><a href='/app/Form/Healthcare Practitioner/{0}'>{0}</a></b>'''.format(practitioner)) frappe.throw(msg, title=_('Missing Configuration')) def get_practitioner_service_item(practitioner, is_inpatient): service_item = None practitioner_charge = None if is_inpatient: service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['inpatient_visit_charge_item', 'inpatient_visit_charge']) else: service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['op_consulting_charge_item', 'op_consulting_charge']) return service_item, practitioner_charge def get_healthcare_service_item(is_inpatient): service_item = None if is_inpatient: service_item = frappe.db.get_single_value('Healthcare Settings', 'inpatient_visit_charge_item') else: service_item = frappe.db.get_single_value('Healthcare Settings', 'op_consulting_charge_item') return service_item def get_practitioner_charge(practitioner, is_inpatient): if is_inpatient: practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, 'inpatient_visit_charge') else: practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, 'op_consulting_charge') if practitioner_charge: return practitioner_charge return False def manage_invoice_submit_cancel(doc, method): if doc.items: for item in doc.items: if item.get('reference_dt') and item.get('reference_dn'): if frappe.get_meta(item.reference_dt).has_field('invoiced'): set_invoiced(item, method, doc.name) if method=='on_submit' and frappe.db.get_single_value('Healthcare Settings', 'create_lab_test_on_si_submit'): create_multiple('Sales Invoice', doc.name) def set_invoiced(item, method, ref_invoice=None): invoiced = False if method=='on_submit': validate_invoiced_on_submit(item) invoiced = True if item.reference_dt == 'Clinical Procedure': service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') if service_item == item.item_code: frappe.db.set_value(item.reference_dt, item.reference_dn, 'consumption_invoiced', invoiced) else: frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced) else: frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced) if item.reference_dt == 'Patient Appointment': if frappe.db.get_value('Patient Appointment', item.reference_dn, 'procedure_template'): dt_from_appointment = 'Clinical Procedure' else: dt_from_appointment = 'Patient Encounter' manage_doc_for_appointment(dt_from_appointment, item.reference_dn, invoiced) elif item.reference_dt == 'Lab Prescription': manage_prescriptions(invoiced, item.reference_dt, item.reference_dn, 'Lab Test', 'lab_test_created') elif item.reference_dt == 'Procedure Prescription': manage_prescriptions(invoiced, item.reference_dt, item.reference_dn, 'Clinical Procedure', 'procedure_created') def validate_invoiced_on_submit(item): if item.reference_dt == 'Clinical Procedure' and \ frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') == item.item_code: is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'consumption_invoiced') else: is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced') if is_invoiced: frappe.throw(_('The item referenced by {0} - {1} is already invoiced').format( item.reference_dt, item.reference_dn)) def manage_prescriptions(invoiced, ref_dt, ref_dn, dt, created_check_field): created = frappe.db.get_value(ref_dt, ref_dn, created_check_field) if created: # Fetch the doc created for the prescription doc_created = frappe.db.get_value(dt, {'prescription': ref_dn}) frappe.db.set_value(dt, doc_created, 'invoiced', invoiced) def check_fee_validity(appointment): if not frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups'): return validity = frappe.db.exists('Fee Validity', { 'practitioner': appointment.practitioner, 'patient': appointment.patient, 'valid_till': ('>=', appointment.appointment_date) }) if not validity: return validity = frappe.get_doc('Fee Validity', validity) return validity def manage_fee_validity(appointment): fee_validity = check_fee_validity(appointment) if fee_validity: if appointment.status == 'Cancelled' and fee_validity.visited > 0: fee_validity.visited -= 1 frappe.db.delete('Fee Validity Reference', {'appointment': appointment.name}) elif fee_validity.status == 'Completed': return else: fee_validity.visited += 1 fee_validity.append('ref_appointments', { 'appointment': appointment.name }) fee_validity.save(ignore_permissions=True) else: fee_validity = create_fee_validity(appointment) return fee_validity def manage_doc_for_appointment(dt_from_appointment, appointment, invoiced): dn_from_appointment = frappe.db.get_value( dt_from_appointment, filters={'appointment': appointment} ) if dn_from_appointment: frappe.db.set_value(dt_from_appointment, dn_from_appointment, 'invoiced', invoiced) @frappe.whitelist() def get_drugs_to_invoice(encounter): encounter = frappe.get_doc('Patient Encounter', encounter) if encounter: patient = frappe.get_doc('Patient', encounter.patient) if patient: if patient.customer: items_to_invoice = [] for drug_line in encounter.drug_prescription: if drug_line.drug_code: qty = 1 if frappe.db.get_value('Item', drug_line.drug_code, 'stock_uom') == 'Nos': qty = drug_line.get_quantity() description = '' if drug_line.dosage and drug_line.period: description = _('{0} for {1}').format(drug_line.dosage, drug_line.period) items_to_invoice.append({ 'drug_code': drug_line.drug_code, 'quantity': qty, 'description': description }) return items_to_invoice else: validate_customer_created(patient) @frappe.whitelist() def get_children(doctype, parent, company, is_root=False): parent_fieldname = "parent_" + doctype.lower().replace(" ", "_") fields = [ "name as value", "is_group as expandable", "lft", "rgt" ] # fields = [ "name", "is_group", "lft", "rgt" ] filters = [["ifnull(`{0}`,'')".format(parent_fieldname), "=", "" if is_root else parent]] if is_root: fields += ["service_unit_type"] if doctype == "Healthcare Service Unit" else [] filters.append(["company", "=", company]) else: fields += ["service_unit_type", "allow_appointments", "inpatient_occupancy", "occupancy_status"] if doctype == "Healthcare Service Unit" else [] fields += [parent_fieldname + " as parent"] hc_service_units = frappe.get_list(doctype, fields=fields, filters=filters) if doctype == "Healthcare Service Unit": for each in hc_service_units: occupancy_msg = "" if each["expandable"] == 1: occupied = False vacant = False child_list = frappe.db.sql( ''' SELECT name, occupancy_status FROM `tabHealthcare Service Unit` WHERE inpatient_occupancy = 1 and lft > %s and rgt < %s ''', (each['lft'], each['rgt'])) for child in child_list: if not occupied: occupied = 0 if child[1] == "Occupied": occupied += 1 if not vacant: vacant = 0 if child[1] == "Vacant": vacant += 1 if vacant and occupied: occupancy_total = vacant + occupied occupancy_msg = str(occupied) + " Occupied out of " + str(occupancy_total) each["occupied_out_of_vacant"] = occupancy_msg return hc_service_units @frappe.whitelist() def get_patient_vitals(patient, from_date=None, to_date=None): if not patient: return vitals = frappe.db.get_all('Vital Signs', filters={ 'docstatus': 1, 'patient': patient }, order_by='signs_date, signs_time', fields=['*']) if len(vitals): return vitals return False @frappe.whitelist() def render_docs_as_html(docs): # docs key value pair {doctype: docname} docs_html = "<div class='col-md-12 col-sm-12 text-muted'>" for doc in docs: docs_html += render_doc_as_html(doc['doctype'], doc['docname'])['html'] + '<br/>' return {'html': docs_html} @frappe.whitelist() def render_doc_as_html(doctype, docname, exclude_fields = []): #render document as html, three column layout will break doc = frappe.get_doc(doctype, docname) meta = frappe.get_meta(doctype) doc_html = "<div class='col-md-12 col-sm-12'>" section_html = '' section_label = '' html = '' sec_on = False col_on = 0 has_data = False for df in meta.fields: #on section break append append previous section and html to doc html if df.fieldtype == "Section Break": if has_data and col_on and sec_on: doc_html += section_html + html + "</div>" elif has_data and not col_on and sec_on: doc_html += "<div class='col-md-12 col-sm-12'\ ><div class='col-md-12 col-sm-12'>" \ + section_html + html +"</div></div>" while col_on: doc_html += "</div>" col_on -= 1 sec_on = True has_data= False col_on = 0 section_html = '' html = '' if df.label: section_label = df.label continue #on column break append html to section html or doc html if df.fieldtype == "Column Break": if sec_on and has_data: section_html += "<div class='col-md-12 col-sm-12'\ ><div class='col-md-6 col\ -sm-6'><b>" + section_label + "</b>" + html + "</div><div \ class='col-md-6 col-sm-6'>" elif has_data: doc_html += "<div class='col-md-12 col-sm-12'><div class='col-m\ d-6 col-sm-6'>" + html + "</div><div class='col-md-6 col-sm-6'>" elif sec_on and not col_on: section_html += "<div class='col-md-6 col-sm-6'>" html = '' col_on += 1 if df.label: html += '<br>' + df.label continue #on table iterate in items and create table based on in_list_view, append to section html or doc html if df.fieldtype == 'Table': items = doc.get(df.fieldname) if not items: continue child_meta = frappe.get_meta(df.options) if not has_data : has_data = True table_head = '' table_row = '' create_head = True for item in items: table_row += '<tr>' for cdf in child_meta.fields: if cdf.in_list_view: if create_head: table_head += '<th>' + cdf.label + '</th>' if item.get(cdf.fieldname): table_row += '<td>' + str(item.get(cdf.fieldname)) \ + '</td>' else: table_row += '<td></td>' create_head = False table_row += '</tr>' if sec_on: section_html += "<table class='table table-condensed \ bordered'>" + table_head + table_row + '</table>' else: html += "<table class='table table-condensed table-bordered'>" \ + table_head + table_row + "</table>" continue #on other field types add label and value to html if not df.hidden and not df.print_hide and doc.get(df.fieldname) and df.fieldname not in exclude_fields: if doc.get(df.fieldname): formatted_value = format_value(doc.get(df.fieldname), meta.get_field(df.fieldname), doc) html += '<br>{0} : {1}'.format(df.label or df.fieldname, formatted_value) if not has_data : has_data = True if sec_on and col_on and has_data: doc_html += section_html + html + '</div></div>' elif sec_on and not col_on and has_data: doc_html += "<div class='col-md-12 col-sm-12'\ ><div class='col-md-12 col-sm-12'>" \ + section_html + html +'</div></div>' if doc_html: doc_html = "<div class='small'><div class='col-md-12 text-right'><a class='btn btn-default btn-xs' href='/app/Form/%s/%s'></a></div>" %(doctype, docname) + doc_html + '</div>' return {'html': doc_html}