# -*- 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 += " {0}".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 += '''Healthcare Settings''' 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) \ + '''Healthcare Settings''') 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) \ + ''' {0}'''.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=None, company=None, is_root=False): parent_fieldname = 'parent_' + doctype.lower().replace(' ', '_') fields = [ 'name as value', 'is_group as expandable', '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'] service_units = frappe.get_list(doctype, fields=fields, filters=filters) for each in service_units: if each['expandable'] == 1: # group node available_count = frappe.db.count('Healthcare Service Unit', filters={ 'parent_healthcare_service_unit': each['value'], 'inpatient_occupancy': 1}) if available_count > 0: occupied_count = frappe.db.count('Healthcare Service Unit', { 'parent_healthcare_service_unit': each['value'], 'inpatient_occupancy': 1, 'occupancy_status': 'Occupied'}) # set occupancy status of group node each['occupied_of_available'] = str( occupied_count) + ' Occupied of ' + str(available_count) return 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 = "
" for doc in docs: docs_html += render_doc_as_html(doc['doctype'], doc['docname'])['html'] + '
' 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 = "
" 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 + "
" elif has_data and not col_on and sec_on: doc_html += "
" \ + section_html + html +"
" while col_on: doc_html += "
" 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 += "
" + section_label + "" + html + "
" elif has_data: doc_html += "
" + html + "
" elif sec_on and not col_on: section_html += "
" html = '' col_on += 1 if df.label: html += '
' + 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 += '' for cdf in child_meta.fields: if cdf.in_list_view: if create_head: table_head += '' + cdf.label + '' if item.get(cdf.fieldname): table_row += '' + str(item.get(cdf.fieldname)) \ + '' else: table_row += '' create_head = False table_row += '' if sec_on: section_html += "" + table_head + table_row + '
' else: html += "" \ + table_head + table_row + "
" 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 += '
{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 + '
' elif sec_on and not col_on and has_data: doc_html += "
" \ + section_html + html +'
' if doc_html: doc_html = "
" %(doctype, docname) + doc_html + '
' return {'html': doc_html} def update_address_links(address, method): ''' Hook validate Address If Patient is linked in Address, also link the associated Customer ''' if 'Healthcare' not in frappe.get_active_domains(): return patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', address.links)) for link in patient_links: customer = frappe.db.get_value('Patient', link.get('link_name'), 'customer') if customer and not address.has_link('Customer', customer): address.append('links', dict(link_doctype = 'Customer', link_name = customer)) def update_patient_email_and_phone_numbers(contact, method): ''' Hook validate Contact Update linked Patients' primary mobile and phone numbers ''' if 'Healthcare' not in frappe.get_active_domains(): return if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone): patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', contact.links)) for link in patient_links: contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1) if contact.email_id and contact.email_id != contact_details.get('email'): frappe.db.set_value('Patient', link.get('link_name'), 'email', contact.email_id) if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'): frappe.db.set_value('Patient', link.get('link_name'), 'mobile', contact.mobile_no) if contact.phone and contact.phone != contact_details.get('phone'): frappe.db.set_value('Patient', link.get('link_name'), 'phone', contact.phone)