Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into payment_entry_validations_and_trigger

This commit is contained in:
Deepesh Garg
2021-08-21 19:19:10 +05:30
1250 changed files with 2718 additions and 2533 deletions

View File

@@ -10,3 +10,6 @@
# This commit just changes spaces to tabs for indentation in some files
5f473611bd6ed57703716244a054d3fb5ba9cd23
# Whitespace trimming throughout codebase
9bb69e711a5da43aaf8c8ecb5601aeffd89dbe5a

View File

@@ -450,5 +450,3 @@ def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr):
return debit_account
else:
return credit_account

View File

@@ -113,5 +113,3 @@ def disable_dimension():
dimension2 = frappe.get_doc("Accounting Dimension", "Location")
dimension2.disabled = 1
dimension2.save()

View File

@@ -105,4 +105,3 @@ def unclear_reference_payment(doctype, docname):
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
return doc.payment_entry

View File

@@ -18,5 +18,3 @@ class CashFlowMapping(Document):
frappe._('You can only select a maximum of one option from the list of check boxes.'),
title='Error'
)

View File

@@ -62,6 +62,3 @@ def create_cost_center(**args):
cc.is_group = args.is_group or 0
cc.parent_cost_center = args.parent_cost_center or "_Test Company - _TC"
cc.insert()

View File

@@ -124,6 +124,3 @@ class TestCouponCode(unittest.TestCase):
so.submit()
self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1)

View File

@@ -39,4 +39,3 @@ class ModeofPayment(Document):
message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \
Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode."
frappe.throw(_(message), title="Not Allowed")

View File

@@ -240,5 +240,3 @@ def get_temporary_opening_account(company=None):
frappe.throw(_("Please add a Temporary Opening account in Chart of Accounts"))
return accounts[0].name

View File

@@ -147,4 +147,3 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")

View File

@@ -26,4 +26,3 @@ QUnit.test("test pricing rule", function(assert) {
() => done()
]);
});

View File

@@ -106,7 +106,6 @@
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate"
},
{
@@ -170,7 +169,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-03-07 11:56:23.424137",
"modified": "2021-08-19 15:49:29.598727",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Promotional Scheme Price Discount",

View File

@@ -72,4 +72,3 @@ QUnit.test("test purchase invoice", function(assert) {
() => done()
]);
});

View File

@@ -26,4 +26,3 @@ QUnit.test("test sales taxes and charges template", function(assert) {
() => done()
]);
});

File diff suppressed because it is too large Load Diff

View File

@@ -40,4 +40,3 @@ QUnit.test("test sales Invoice", function(assert) {
() => done()
]);
});

View File

@@ -33,4 +33,3 @@ QUnit.test("test sales invoice with margin", function(assert) {
() => done()
]);
});

View File

@@ -54,4 +54,3 @@ QUnit.test("test sales Invoice with payment", function(assert) {
() => done()
]);
});

View File

@@ -49,4 +49,3 @@ QUnit.test("test sales Invoice with payment request", function(assert) {
() => done()
]);
});

View File

@@ -42,4 +42,3 @@ QUnit.test("test sales Invoice with serialize item", function(assert) {
() => done()
]);
});

View File

@@ -26,4 +26,3 @@ QUnit.test("test sales taxes and charges template", function(assert) {
() => done()
]);
});

View File

@@ -34,4 +34,3 @@ QUnit.test("test Shipping Rule", function(assert) {
() => done()
]);
});

View File

@@ -34,4 +34,3 @@ QUnit.test("test Shipping Rule", function(assert) {
() => done()
]);
});

View File

@@ -630,5 +630,3 @@ class TestSubscription(unittest.TestCase):
subscription.process()
self.assertEqual(len(subscription.invoices), 1)

View File

@@ -286,6 +286,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren
.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
def validate_party_accounts(doc):
companies = []
for account in doc.get("accounts"):
@@ -446,6 +447,10 @@ def get_payment_terms_template(party_name, party_type, company=None):
return template
def validate_party_frozen_disabled(party_type, party_name):
if frappe.flags.ignore_party_validation:
return
if party_type and party_name:
if party_type in ("Customer", "Supplier"):
party = frappe.get_cached_value(party_type, party_name, ["is_frozen", "disabled"], as_dict=True)

View File

@@ -27,4 +27,3 @@
{{ _("Authorized Signatory") }}
</p>
</div>

View File

@@ -62,8 +62,3 @@ def make_sales_invoice():
income_account = 'Sales - _TC2',
expense_account = 'Cost of Goods Sold - _TC2',
cost_center = 'Main - _TC2')

View File

@@ -136,4 +136,3 @@ frappe.query_reports["Accounts Payable"] = {
}
erpnext.utils.add_dimensions('Accounts Payable', 9);

View File

@@ -105,4 +105,3 @@ frappe.query_reports["Accounts Payable Summary"] = {
}
erpnext.utils.add_dimensions('Accounts Payable Summary', 9);

View File

@@ -12,4 +12,3 @@ def execute(filters=None):
"naming_by": ["Buying Settings", "supp_master_name"],
}
return AccountsReceivableSummary(filters).run(args)

View File

@@ -200,4 +200,3 @@ frappe.query_reports["Accounts Receivable"] = {
}
erpnext.utils.add_dimensions('Accounts Receivable', 9);

View File

@@ -93,4 +93,3 @@ def make_credit_note(docname):
cost_center = 'Main - _TC2',
is_return = 1,
return_against = docname)

View File

@@ -92,4 +92,3 @@ frappe.query_reports["Budget Variance Report"] = {
erpnext.dimension_filters.forEach((dimension) => {
frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]);
});

View File

@@ -399,4 +399,3 @@ def get_chart_data(filters, columns, data):
},
'type' : 'bar'
}

View File

@@ -176,4 +176,3 @@ frappe.query_reports["General Ledger"] = {
}
erpnext.utils.add_dimensions('General Ledger', 15)

View File

@@ -76,7 +76,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
'company': d.company,
'sales_order': d.sales_order,
'delivery_note': d.delivery_note,
'income_account': d.unrealized_profit_loss_account or d.income_account,
'income_account': d.unrealized_profit_loss_account if d.is_internal_customer == 1 else d.income_account,
'cost_center': d.cost_center,
'stock_qty': d.stock_qty,
'stock_uom': d.stock_uom
@@ -380,6 +380,7 @@ def get_items(filters, additional_query_columns):
`tabSales Invoice Item`.name, `tabSales Invoice Item`.parent,
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
`tabSales Invoice`.unrealized_profit_loss_account,
`tabSales Invoice`.is_internal_customer,
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
@@ -625,7 +626,3 @@ def add_sub_total_row(item, total_row_map, group_by_value, tax_columns):
for tax in tax_columns:
total_row.setdefault(frappe.scrub(tax + ' Amount'), 0.0)
total_row[frappe.scrub(tax + ' Amount')] += flt(item[frappe.scrub(tax + ' Amount')])

View File

@@ -69,4 +69,3 @@ frappe.query_reports["Sales Register"] = {
}
erpnext.utils.add_dimensions('Sales Register', 7);

View File

@@ -84,7 +84,7 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No
# Add amount in unrealized account
for account in unrealized_profit_loss_accounts:
row.update({
frappe.scrub(account): flt(internal_invoice_map.get((inv.name, account)))
frappe.scrub(account+"_unrealized"): flt(internal_invoice_map.get((inv.name, account)))
})
# net total
@@ -258,6 +258,7 @@ def get_columns(invoice_list, additional_table_columns):
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabSales Invoice` where docstatus = 1 and name in (%s)
and is_internal_customer = 1
and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account""" %
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
@@ -284,7 +285,7 @@ def get_columns(invoice_list, additional_table_columns):
for account in unrealized_profit_loss_accounts:
unrealized_profit_loss_account_columns.append({
"label": account,
"fieldname": frappe.scrub(account),
"fieldname": frappe.scrub(account+"_unrealized"),
"fieldtype": "Currency",
"options": "currency",
"width": 120

View File

@@ -110,6 +110,3 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
erpnext.utils.add_dimensions('Trial Balance', 6);
});

View File

@@ -351,6 +351,7 @@ def reconcile_against_document(args):
# cancel advance entry
doc = frappe.get_doc(d.voucher_type, d.voucher_no)
frappe.flags.ignore_party_validation = True
doc.make_gl_entries(cancel=1, adv_adj=1)
# update ref in advance entry
@@ -362,6 +363,7 @@ def reconcile_against_document(args):
# re-submit advance entry
doc = frappe.get_doc(d.voucher_type, d.voucher_no)
doc.make_gl_entries(cancel = 0, adv_adj =1)
frappe.flags.ignore_party_validation = False
if d.voucher_type in ('Payment Entry', 'Journal Entry'):
doc.update_expense_claim()

View File

@@ -36,4 +36,3 @@ QUnit.test("test: Disease", function (assert) {
]);
});

View File

@@ -10,4 +10,3 @@ frappe.listview_settings['Asset Repair'] = {
}
}
};

View File

@@ -93,5 +93,3 @@ var loadAllStandings = function(frm) {
}
});
};

View File

@@ -128,4 +128,3 @@ valid_scorecard = [
"weighting_function":"{total_score} * max( 0, min ( 1 , (12 - {period_number}) / 12) )"
}
]

View File

@@ -109,4 +109,3 @@ def make_supplier_scorecard(source_name, target_doc=None):
}, target_doc, post_process, ignore_permissions=True)
return doc

View File

@@ -268,4 +268,3 @@ def get_columns(filters):
])
return columns

View File

@@ -102,4 +102,3 @@ def get_linked_material_requests(items):
mr_list.append(material_request)
return mr_list

View File

@@ -1863,7 +1863,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
qty_unchanged = prev_qty == new_qty
uom_unchanged = prev_uom == new_uom
conversion_factor_unchanged = prev_con_fac == new_con_fac
date_unchanged = prev_date == new_date if prev_date and new_date else False # in case of delivery note etc
date_unchanged = prev_date == getdate(new_date) if prev_date and new_date else False # in case of delivery note etc
if rate_unchanged and qty_unchanged and conversion_factor_unchanged and uom_unchanged and date_unchanged:
continue

View File

@@ -344,4 +344,3 @@ def create_variant_doc_for_quick_entry(template, args):
variant.name = variant.item_code
validate_item_variant_attributes(variant, args)
return variant.as_dict()

View File

@@ -526,6 +526,9 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
if meta.is_tree:
query_filters.append(['is_group', '=', 0])
if meta.has_field('disabled'):
query_filters.append(['disabled', '!=', 1])
if meta.has_field('company'):
query_filters.append(['company', '=', filters.get('company')])

View File

@@ -235,4 +235,3 @@ def _get_employee_from_user(user):
# frappe.db.exists returns a tuple of a tuple
return frappe.get_doc('Employee', employee_docname[0][0])
return None

View File

@@ -16,4 +16,3 @@ frappe.query_reports["Campaign Efficiency"] = {
}
]
};

View File

@@ -20,5 +20,3 @@ frappe.query_reports["Lead Conversion Time"] = {
},
]
};

View File

@@ -155,4 +155,3 @@ def get_conditions(filters) :
conditions.append(" and `tabLead`.status=%(status)s")
return " ".join(conditions) if conditions else ""

View File

@@ -42,7 +42,3 @@ class AssessmentResult(Document):
"student":self.student, "assessment_plan":self.assessment_plan, "docstatus":("!=", 2)})
if assessment_result:
frappe.throw(_("Assessment Result record {0} already exists.").format(getlink("Assessment Result",assessment_result[0].name)))

View File

@@ -16,4 +16,3 @@ class TestAssessmentResult(unittest.TestCase):
grade = get_grade("_Test Grading Scale", 70)
self.assertEqual("B", grade)

View File

@@ -39,6 +39,3 @@ class TestCourseEnrollment(unittest.TestCase):
doc = frappe.get_doc("Program Enrollment", entry.name)
doc.cancel()
doc.delete()

View File

@@ -47,4 +47,3 @@ class CourseSchedule(Document):
validate_overlap_for(self, "Assessment Plan", "room")
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)

View File

@@ -174,4 +174,3 @@ def get_students(doctype, txt, searchfield, start, page_len, filters):
tuple(students + ["%%%s%%" % txt, start, page_len]
)
)

View File

@@ -128,4 +128,3 @@ def fetch_students(doctype, txt, searchfield, start, page_len, filters):
order by idx desc, name
limit %s, %s""".format(searchfield),
tuple(["%%%s%%" % txt, "%%%s%%" % txt, start, page_len]))

View File

@@ -121,4 +121,3 @@ def get_chart_data(data):
},
'type': 'bar'
}

View File

@@ -350,4 +350,3 @@ def is_sync_complete(shopify_settings, order):
return getdate(shopify_settings.to_date) < getdate(order.get('created_at'))
else:
return cstr(order.get('id')) == cstr(shopify_settings.to_order_id)

View File

@@ -1,3 +1,2 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt

View File

@@ -11,7 +11,7 @@ test_dependencies = ['Item']
class TestClinicalProcedure(unittest.TestCase):
def test_procedure_template_item(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
procedure_template = create_clinical_procedure_template()
self.assertTrue(frappe.db.exists('Item', procedure_template.item))
@@ -20,7 +20,7 @@ class TestClinicalProcedure(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Item', procedure_template.item, 'disabled'), 1)
def test_consumables(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
procedure_template = create_clinical_procedure_template()
procedure_template.allow_stock_consumption = 1
consumable = create_consumable()

View File

@@ -188,4 +188,3 @@ frappe.tour['Clinical Procedure Template'] = [
description: __('You can also set the Medical Department for the template. After saving the document, an Item will automatically be created for billing this Clinical Procedure. You can then use this template while creating Clinical Procedures for Patients. Templates save you from filling up redundant data every single time. You can also create templates for other operations like Lab Tests, Therapy Sessions, etc.')
}
];

View File

@@ -118,4 +118,3 @@ def change_item_code_from_template(item_code, doc):
rename_doc('Item', doc.item_code, item_code, ignore_permissions=True)
frappe.db.set_value('Clinical Procedure Template', doc.name, 'item_code', item_code)
return

View File

@@ -12,4 +12,3 @@ class ExerciseType(Document):
self.name = ' - '.join(filter(None, [self.exercise_name, self.difficulty_level]))
else:
self.name = self.exercise_name

View File

@@ -27,7 +27,7 @@ class TestFeeValidity(unittest.TestCase):
healthcare_settings.automate_appointment_invoicing = 1
healthcare_settings.op_consulting_charge_item = item
healthcare_settings.save(ignore_permissions=True)
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
# appointment should not be invoiced. Check Fee Validity created for new patient
appointment = create_appointment(patient, practitioner, nowdate())

View File

@@ -142,4 +142,3 @@ frappe.tour['Healthcare Practitioner'] = [
description: __('If this Healthcare Practitioner also works for the In-Patient Department, set the inpatient visit charge for this Practitioner.')
}
];

View File

@@ -7,8 +7,8 @@ frappe.ui.form.on('Healthcare Service Unit', {
// get query select healthcare service unit
frm.fields_dict['parent_healthcare_service_unit'].get_query = function(doc) {
return{
filters:[
return {
filters: [
['Healthcare Service Unit', 'is_group', '=', 1],
['Healthcare Service Unit', 'name', '!=', doc.healthcare_service_unit_name]
]
@@ -21,6 +21,14 @@ frappe.ui.form.on('Healthcare Service Unit', {
frm.add_custom_button(__('Healthcare Service Unit Tree'), function() {
frappe.set_route('Tree', 'Healthcare Service Unit');
});
frm.set_query('warehouse', function() {
return {
filters: {
'company': frm.doc.company
}
};
});
},
set_root_readonly: function(frm) {
// read-only for root healthcare service unit
@@ -43,5 +51,10 @@ frappe.ui.form.on('Healthcare Service Unit', {
else {
frm.set_df_property('service_unit_type', 'reqd', 1);
}
},
overlap_appointments: function(frm) {
if (frm.doc.overlap_appointments == 0) {
frm.set_value('service_unit_capacity', '');
}
}
});

View File

@@ -16,6 +16,7 @@
"service_unit_type",
"allow_appointments",
"overlap_appointments",
"service_unit_capacity",
"inpatient_occupancy",
"occupancy_status",
"column_break_9",
@@ -31,6 +32,8 @@
{
"fieldname": "healthcare_service_unit_name",
"fieldtype": "Data",
"hide_days": 1,
"hide_seconds": 1,
"in_global_search": 1,
"in_list_view": 1,
"label": "Service Unit",
@@ -41,6 +44,8 @@
"bold": 1,
"fieldname": "parent_healthcare_service_unit",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Parent Service Unit",
@@ -52,6 +57,8 @@
"depends_on": "eval:doc.inpatient_occupancy != 1 && doc.allow_appointments != 1",
"fieldname": "is_group",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Is Group"
},
{
@@ -59,6 +66,8 @@
"depends_on": "eval:doc.is_group != 1",
"fieldname": "service_unit_type",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Service Unit Type",
"options": "Healthcare Service Unit Type"
},
@@ -68,6 +77,8 @@
"fetch_from": "service_unit_type.allow_appointments",
"fieldname": "allow_appointments",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"label": "Allow Appointments",
"no_copy": 1,
@@ -79,6 +90,8 @@
"fetch_from": "service_unit_type.overlap_appointments",
"fieldname": "overlap_appointments",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Allow Overlap",
"no_copy": 1,
"read_only": 1
@@ -90,6 +103,8 @@
"fetch_from": "service_unit_type.inpatient_occupancy",
"fieldname": "inpatient_occupancy",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"label": "Inpatient Occupancy",
"no_copy": 1,
@@ -100,6 +115,8 @@
"depends_on": "eval:doc.inpatient_occupancy == 1",
"fieldname": "occupancy_status",
"fieldtype": "Select",
"hide_days": 1,
"hide_seconds": 1,
"label": "Occupancy Status",
"no_copy": 1,
"options": "Vacant\nOccupied",
@@ -107,13 +124,17 @@
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1
},
{
"bold": 1,
"depends_on": "eval:doc.is_group != 1",
"fieldname": "warehouse",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Warehouse",
"no_copy": 1,
"options": "Warehouse"
@@ -121,6 +142,8 @@
{
"fieldname": "company",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"ignore_user_permissions": 1,
"in_list_view": 1,
"in_standard_filter": 1,
@@ -134,6 +157,8 @@
"fieldname": "lft",
"fieldtype": "Int",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "lft",
"no_copy": 1,
"print_hide": 1,
@@ -143,6 +168,8 @@
"fieldname": "rgt",
"fieldtype": "Int",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "rgt",
"no_copy": 1,
"print_hide": 1,
@@ -152,6 +179,8 @@
"fieldname": "old_parent",
"fieldtype": "Link",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"ignore_user_permissions": 1,
"label": "Old Parent",
"no_copy": 1,
@@ -163,14 +192,26 @@
"collapsible": 1,
"fieldname": "tree_details_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Tree Details"
},
{
"depends_on": "eval:doc.overlap_appointments == 1",
"fieldname": "service_unit_capacity",
"fieldtype": "Int",
"label": "Service Unit Capacity",
"mandatory_depends_on": "eval:doc.overlap_appointments == 1",
"non_negative": 1
}
],
"is_tree": 1,
"links": [],
"modified": "2020-05-20 18:26:56.065543",
"modified": "2021-08-19 14:09:11.643464",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Service Unit",
"nsm_parent_field": "parent_healthcare_service_unit",
"owner": "Administrator",
"permissions": [
{

View File

@@ -5,14 +5,21 @@
from __future__ import unicode_literals
from frappe.utils.nestedset import NestedSet
from frappe.utils import cint, cstr
import frappe
from frappe import _
import json
class HealthcareServiceUnit(NestedSet):
nsm_parent_field = 'parent_healthcare_service_unit'
def validate(self):
self.set_service_unit_properties()
def autoname(self):
if self.company:
suffix = " - " + frappe.get_cached_value('Company', self.company, "abbr")
suffix = " - " + frappe.get_cached_value('Company', self.company, 'abbr')
if not self.healthcare_service_unit_name.endswith(suffix):
self.name = self.healthcare_service_unit_name + suffix
else:
@@ -22,16 +29,86 @@ class HealthcareServiceUnit(NestedSet):
super(HealthcareServiceUnit, self).on_update()
self.validate_one_root()
def after_insert(self):
def set_service_unit_properties(self):
if self.is_group:
self.allow_appointments = 0
self.overlap_appointments = 0
self.inpatient_occupancy = 0
elif self.service_unit_type:
self.allow_appointments = False
self.overlap_appointments = False
self.inpatient_occupancy = False
self.service_unit_capacity = 0
self.occupancy_status = ''
self.service_unit_type = ''
elif self.service_unit_type != '':
service_unit_type = frappe.get_doc('Healthcare Service Unit Type', self.service_unit_type)
self.allow_appointments = service_unit_type.allow_appointments
self.overlap_appointments = service_unit_type.overlap_appointments
self.inpatient_occupancy = service_unit_type.inpatient_occupancy
if self.inpatient_occupancy:
if self.inpatient_occupancy and self.occupancy_status != '':
self.occupancy_status = 'Vacant'
self.overlap_appointments = 0
if service_unit_type.overlap_appointments:
self.overlap_appointments = True
else:
self.overlap_appointments = False
self.service_unit_capacity = 0
if self.overlap_appointments:
if not self.service_unit_capacity:
frappe.throw(_('Please set a valid Service Unit Capacity to enable Overlapping Appointments'),
title=_('Mandatory'))
@frappe.whitelist()
def add_multiple_service_units(parent, data):
'''
parent - parent service unit under which the service units are to be created
data (dict) - company, healthcare_service_unit_name, count, service_unit_type, warehouse, service_unit_capacity
'''
if not parent or not data:
return
data = json.loads(data)
company = data.get('company') or \
frappe.defaults.get_defaults().get('company') or \
frappe.db.get_single_value('Global Defaults', 'default_company')
if not data.get('healthcare_service_unit_name') or not company:
frappe.throw(_('Service Unit Name and Company are mandatory to create Healthcare Service Units'),
title=_('Missing Required Fields'))
count = cint(data.get('count') or 0)
if count <= 0:
frappe.throw(_('Number of Service Units to be created should at least be 1'),
title=_('Invalid Number of Service Units'))
capacity = cint(data.get('service_unit_capacity') or 1)
service_unit = {
'doctype': 'Healthcare Service Unit',
'parent_healthcare_service_unit': parent,
'service_unit_type': data.get('service_unit_type') or None,
'service_unit_capacity': capacity if capacity > 0 else 1,
'warehouse': data.get('warehouse') or None,
'company': company
}
service_unit_name = '{}'.format(data.get('healthcare_service_unit_name').strip(' -'))
last_suffix = frappe.db.sql("""SELECT
IFNULL(MAX(CAST(SUBSTRING(name FROM %(start)s FOR 4) AS UNSIGNED)), 0)
FROM `tabHealthcare Service Unit`
WHERE name like %(prefix)s AND company=%(company)s""",
{'start': len(service_unit_name)+2, 'prefix': '{}-%'.format(service_unit_name), 'company': company},
as_list=1)[0][0]
start_suffix = cint(last_suffix) + 1
failed_list = []
for i in range(start_suffix, count + start_suffix):
# name to be in the form WARD-####
service_unit['healthcare_service_unit_name'] = '{}-{}'.format(service_unit_name, cstr('%0*d' % (4, i)))
service_unit_doc = frappe.get_doc(service_unit)
try:
service_unit_doc.insert()
except Exception:
failed_list.append(service_unit['healthcare_service_unit_name'])
return failed_list

View File

@@ -1,35 +1,185 @@
frappe.treeview_settings["Healthcare Service Unit"] = {
breadcrumbs: "Healthcare Service Unit",
title: __("Healthcare Service Unit"),
frappe.provide("frappe.treeview_settings");
frappe.treeview_settings['Healthcare Service Unit'] = {
breadcrumbs: 'Healthcare Service Unit',
title: __('Service Unit Tree'),
get_tree_root: false,
filters: [{
fieldname: "company",
fieldtype: "Select",
options: erpnext.utils.get_tree_options("company"),
label: __("Company"),
default: erpnext.utils.get_tree_default("company")
}],
get_tree_nodes: 'erpnext.healthcare.utils.get_children',
ignore_fields:["parent_healthcare_service_unit"],
onrender: function(node) {
if (node.data.occupied_out_of_vacant!==undefined) {
$('<span class="balance-area pull-right">'
+ " " + node.data.occupied_out_of_vacant
filters: [{
fieldname: 'company',
fieldtype: 'Select',
options: erpnext.utils.get_tree_options('company'),
label: __('Company'),
default: erpnext.utils.get_tree_default('company')
}],
fields: [
{
fieldtype: 'Data', fieldname: 'healthcare_service_unit_name', label: __('New Service Unit Name'),
reqd: true
},
{
fieldtype: 'Check', fieldname: 'is_group', label: __('Is Group'),
description: __("Child nodes can be only created under 'Group' type nodes")
},
{
fieldtype: 'Link', fieldname: 'service_unit_type', label: __('Service Unit Type'),
options: 'Healthcare Service Unit Type', description: __('Type of the new Service Unit'),
depends_on: 'eval:!doc.is_group', default: '',
onchange: () => {
if (cur_dialog) {
if (cur_dialog.fields_dict.service_unit_type.value) {
frappe.db.get_value('Healthcare Service Unit Type',
cur_dialog.fields_dict.service_unit_type.value, 'overlap_appointments')
.then(r => {
if (r.message.overlap_appointments) {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', false);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', true);
} else {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', true);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', false);
}
});
} else {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', true);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', false);
}
}
}
},
{
fieldtype: 'Int', fieldname: 'service_unit_capacity', label: __('Service Unit Capacity'),
description: __('Sets the number of concurrent appointments allowed'), reqd: false,
depends_on: "eval:!doc.is_group && doc.service_unit_type != ''", hidden: true
},
{
fieldtype: 'Link', fieldname: 'warehouse', label: __('Warehouse'), options: 'Warehouse',
description: __('Optional, if you want to manage stock separately for this Service Unit'),
depends_on: 'eval:!doc.is_group'
},
{
fieldtype: 'Link', fieldname: 'company', label: __('Company'), options: 'Company', reqd: true,
default: () => {
return cur_page.page.page.fields_dict.company.value;
}
}
],
ignore_fields: ['parent_healthcare_service_unit'],
onrender: function (node) {
if (node.data.occupied_of_available !== undefined) {
$("<span class='balance-area pull-right text-muted small'>"
+ ' ' + node.data.occupied_of_available
+ '</span>').insertBefore(node.$ul);
}
if (node.data && node.data.inpatient_occupancy!==undefined) {
if (node.data && node.data.inpatient_occupancy !== undefined) {
if (node.data.inpatient_occupancy == 1) {
if (node.data.occupancy_status == "Occupied") {
$('<span class="balance-area pull-right">'
+ " " + node.data.occupancy_status
if (node.data.occupancy_status == 'Occupied') {
$("<span class='balance-area pull-right small'>"
+ ' ' + node.data.occupancy_status
+ '</span>').insertBefore(node.$ul);
}
if (node.data.occupancy_status == "Vacant") {
$('<span class="balance-area pull-right">'
+ " " + node.data.occupancy_status
if (node.data.occupancy_status == 'Vacant') {
$("<span class='balance-area pull-right text-muted small'>"
+ ' ' + node.data.occupancy_status
+ '</span>').insertBefore(node.$ul);
}
}
}
},
post_render: function (treeview) {
frappe.treeview_settings['Healthcare Service Unit'].treeview = {};
$.extend(frappe.treeview_settings['Healthcare Service Unit'].treeview, treeview);
},
toolbar: [
{
label: __('Add Multiple'),
condition: function (node) {
return node.expandable;
},
click: function (node) {
const dialog = new frappe.ui.Dialog({
title: __('Add Multiple Service Units'),
fields: [
{
fieldtype: 'Data', fieldname: 'healthcare_service_unit_name', label: __('Service Unit Name'),
reqd: true, description: __("Will be serially suffixed to maintain uniquness. Example: 'Ward' will be named as 'Ward-####'"),
},
{
fieldtype: 'Int', fieldname: 'count', label: __('Number of Service Units'),
reqd: true
},
{
fieldtype: 'Link', fieldname: 'service_unit_type', label: __('Service Unit Type'),
options: 'Healthcare Service Unit Type', description: __('Type of the new Service Unit'),
depends_on: 'eval:!doc.is_group', default: '', reqd: true,
onchange: () => {
if (cur_dialog) {
if (cur_dialog.fields_dict.service_unit_type.value) {
frappe.db.get_value('Healthcare Service Unit Type',
cur_dialog.fields_dict.service_unit_type.value, 'overlap_appointments')
.then(r => {
if (r.message.overlap_appointments) {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', false);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', true);
} else {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', true);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', false);
}
});
} else {
cur_dialog.set_df_property('service_unit_capacity', 'hidden', true);
cur_dialog.set_df_property('service_unit_capacity', 'reqd', false);
}
}
}
},
{
fieldtype: 'Int', fieldname: 'service_unit_capacity', label: __('Service Unit Capacity'),
description: __('Sets the number of concurrent appointments allowed'), reqd: false,
depends_on: "eval:!doc.is_group && doc.service_unit_type != ''", hidden: true
},
{
fieldtype: 'Link', fieldname: 'warehouse', label: __('Warehouse'), options: 'Warehouse',
description: __('Optional, if you want to manage stock separately for this Service Unit'),
},
{
fieldtype: 'Link', fieldname: 'company', label: __('Company'), options: 'Company', reqd: true,
default: () => {
return cur_page.page.page.fields_dict.company.get_value();
}
}
],
primary_action: () => {
dialog.hide();
let vals = dialog.get_values();
if (!vals) return;
return frappe.call({
method: 'erpnext.healthcare.doctype.healthcare_service_unit.healthcare_service_unit.add_multiple_service_units',
args: {
parent: node.data.value,
data: vals
},
callback: function (r) {
if (!r.exc && r.message) {
frappe.treeview_settings['Healthcare Service Unit'].treeview.tree.load_children(node, true);
frappe.show_alert({
message: __('{0} Service Units created', [vals.count - r.message.length]),
indicator: 'green'
});
} else {
frappe.msgprint(__('Could not create Service Units'));
}
},
freeze: true,
freeze_message: __('Creating {0} Service Units', [vals.count])
});
},
primary_action_label: __('Create')
});
dialog.show();
}
}
],
extend_toolbar: true
};

View File

@@ -68,8 +68,8 @@ let change_item_code = function(frm, doc) {
if (values) {
frappe.call({
"method": "erpnext.healthcare.doctype.healthcare_service_unit_type.healthcare_service_unit_type.change_item_code",
"args": {item: doc.item, item_code: values['item_code'], doc_name: doc.name},
callback: function () {
"args": { item: doc.item, item_code: values['item_code'], doc_name: doc.name },
callback: function() {
frm.reload_doc();
}
});

View File

@@ -29,6 +29,8 @@
{
"fieldname": "service_unit_type",
"fieldtype": "Data",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1,
"label": "Service Unit Type",
"no_copy": 1,
@@ -41,6 +43,8 @@
"depends_on": "eval:doc.inpatient_occupancy != 1",
"fieldname": "allow_appointments",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Allow Appointments"
},
{
@@ -49,6 +53,8 @@
"depends_on": "eval:doc.allow_appointments == 1 && doc.inpatient_occupany != 1",
"fieldname": "overlap_appointments",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Allow Overlap"
},
{
@@ -57,6 +63,8 @@
"depends_on": "eval:doc.allow_appointments != 1",
"fieldname": "inpatient_occupancy",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Inpatient Occupancy"
},
{
@@ -65,17 +73,23 @@
"depends_on": "eval:doc.inpatient_occupancy == 1 && doc.allow_appointments != 1",
"fieldname": "is_billable",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Is Billable"
},
{
"depends_on": "is_billable",
"fieldname": "item_details",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Item Details"
},
{
"fieldname": "item",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Item",
"no_copy": 1,
"options": "Item",
@@ -84,6 +98,8 @@
{
"fieldname": "item_code",
"fieldtype": "Data",
"hide_days": 1,
"hide_seconds": 1,
"label": "Item Code",
"mandatory_depends_on": "eval: doc.is_billable == 1",
"no_copy": 1
@@ -91,6 +107,8 @@
{
"fieldname": "item_group",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Item Group",
"mandatory_depends_on": "eval: doc.is_billable == 1",
"options": "Item Group"
@@ -98,6 +116,8 @@
{
"fieldname": "uom",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "UOM",
"mandatory_depends_on": "eval: doc.is_billable == 1",
"options": "UOM"
@@ -105,28 +125,38 @@
{
"fieldname": "no_of_hours",
"fieldtype": "Int",
"hide_days": 1,
"hide_seconds": 1,
"label": "UOM Conversion in Hours",
"mandatory_depends_on": "eval: doc.is_billable == 1"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1
},
{
"fieldname": "rate",
"fieldtype": "Currency",
"hide_days": 1,
"hide_seconds": 1,
"label": "Rate / UOM"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Disabled",
"no_copy": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "Description"
},
{
@@ -134,11 +164,13 @@
"fieldname": "change_in_item",
"fieldtype": "Check",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "Change in Item"
}
],
"links": [],
"modified": "2020-05-20 15:31:09.627516",
"modified": "2021-08-19 17:52:30.266667",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Service Unit Type",

View File

@@ -140,4 +140,3 @@ def create_ipme(filters, update_stock=0):
ipme = ipme.get_medication_orders()
return ipme

View File

@@ -151,7 +151,7 @@ def get_healthcare_service_unit(unit_name=None):
if not service_unit:
service_unit = frappe.new_doc("Healthcare Service Unit")
service_unit.healthcare_service_unit_name = unit_name or "Test Service Unit Ip Occupancy"
service_unit.healthcare_service_unit_name = unit_name or "_Test Service Unit Ip Occupancy"
service_unit.company = "_Test Company"
service_unit.service_unit_type = get_service_unit_type()
service_unit.inpatient_occupancy = 1
@@ -159,12 +159,12 @@ def get_healthcare_service_unit(unit_name=None):
service_unit.is_group = 0
service_unit_parent_name = frappe.db.exists({
"doctype": "Healthcare Service Unit",
"healthcare_service_unit_name": "All Healthcare Service Units",
"healthcare_service_unit_name": "_Test All Healthcare Service Units",
"is_group": 1
})
if not service_unit_parent_name:
parent_service_unit = frappe.new_doc("Healthcare Service Unit")
parent_service_unit.healthcare_service_unit_name = "All Healthcare Service Units"
parent_service_unit.healthcare_service_unit_name = "_Test All Healthcare Service Units"
parent_service_unit.is_group = 1
parent_service_unit.save(ignore_permissions = True)
service_unit.parent_healthcare_service_unit = parent_service_unit.name
@@ -180,7 +180,7 @@ def get_service_unit_type():
if not service_unit_type:
service_unit_type = frappe.new_doc("Healthcare Service Unit Type")
service_unit_type.service_unit_type = "Test Service Unit Type Ip Occupancy"
service_unit_type.service_unit_type = "_Test Service Unit Type Ip Occupancy"
service_unit_type.inpatient_occupancy = 1
service_unit_type.save(ignore_permissions = True)
return service_unit_type.name

View File

@@ -26,31 +26,39 @@ frappe.ui.form.on('Patient', {
}
if (frm.doc.patient_name && frappe.user.has_role('Physician')) {
frm.add_custom_button(__('Patient Progress'), function() {
frappe.route_options = {'patient': frm.doc.name};
frappe.set_route('patient-progress');
}, __('View'));
frm.add_custom_button(__('Patient History'), function() {
frappe.route_options = {'patient': frm.doc.name};
frappe.set_route('patient_history');
},'View');
}, __('View'));
}
if (!frm.doc.__islocal && (frappe.user.has_role('Nursing User') || frappe.user.has_role('Physician'))) {
frm.add_custom_button(__('Vital Signs'), function () {
create_vital_signs(frm);
}, 'Create');
frm.add_custom_button(__('Medical Record'), function () {
create_medical_record(frm);
}, 'Create');
frm.add_custom_button(__('Patient Encounter'), function () {
create_encounter(frm);
}, 'Create');
frm.toggle_enable(['customer'], 0); // ToDo, allow change only if no transactions booked or better, add merge option
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Patient'};
frm.toggle_display(['address_html', 'contact_html'], !frm.is_new());
if (!frm.is_new()) {
if ((frappe.user.has_role('Nursing User') || frappe.user.has_role('Physician'))) {
frm.add_custom_button(__('Medical Record'), function () {
create_medical_record(frm);
}, 'Create');
frm.toggle_enable(['customer'], 0);
}
frappe.contacts.render_address_and_contact(frm);
erpnext.utils.set_party_dashboard_indicators(frm);
} else {
frappe.contacts.clear_address_and_contact(frm);
}
},
onload: function (frm) {
if (!frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html('');
}
if (frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(frm.doc.dob)}`);
} else {
$(frm.fields_dict['age_html'].wrapper).html('');
}
}
});
@@ -59,16 +67,14 @@ frappe.ui.form.on('Patient', 'dob', function(frm) {
if (frm.doc.dob) {
let today = new Date();
let birthDate = new Date(frm.doc.dob);
if (today < birthDate){
if (today < birthDate) {
frappe.msgprint(__('Please select a valid Date'));
frappe.model.set_value(frm.doctype,frm.docname, 'dob', '');
}
else {
} else {
let age_str = get_age(frm.doc.dob);
$(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${age_str}`);
}
}
else {
} else {
$(frm.fields_dict['age_html'].wrapper).html('');
}
});

View File

@@ -1,6 +1,6 @@
{
"actions": [],
"allow_copy": 1,
"allow_events_in_timeline": 1,
"allow_import": 1,
"allow_rename": 1,
"autoname": "naming_series:",
@@ -24,12 +24,19 @@
"image",
"column_break_14",
"status",
"uid",
"inpatient_record",
"inpatient_status",
"report_preference",
"mobile",
"email",
"phone",
"email",
"invite_user",
"user_id",
"address_contacts",
"address_html",
"column_break_22",
"contact_html",
"customer_details_section",
"customer",
"customer_group",
@@ -74,6 +81,7 @@
"fieldtype": "Select",
"in_preview": 1,
"label": "Inpatient Status",
"no_copy": 1,
"options": "\nAdmission Scheduled\nAdmitted\nDischarge Scheduled",
"read_only": 1
},
@@ -81,6 +89,7 @@
"fieldname": "inpatient_record",
"fieldtype": "Link",
"label": "Inpatient Record",
"no_copy": 1,
"options": "Inpatient Record",
"read_only": 1
},
@@ -101,6 +110,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Full Name",
"no_copy": 1,
"read_only": 1,
"search_index": 1
},
@@ -118,6 +128,7 @@
"fieldtype": "Select",
"in_preview": 1,
"label": "Blood Group",
"no_copy": 1,
"options": "\nA Positive\nA Negative\nAB Positive\nAB Negative\nB Positive\nB Negative\nO Positive\nO Negative"
},
{
@@ -125,7 +136,8 @@
"fieldname": "dob",
"fieldtype": "Date",
"in_preview": 1,
"label": "Date of birth"
"label": "Date of birth",
"no_copy": 1
},
{
"fieldname": "age_html",
@@ -167,6 +179,7 @@
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Customer",
"no_copy": 1,
"options": "Customer",
"set_only_once": 1
},
@@ -183,6 +196,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Mobile",
"no_copy": 1,
"options": "Phone"
},
{
@@ -192,6 +206,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Email",
"no_copy": 1,
"options": "Email"
},
{
@@ -199,6 +214,7 @@
"fieldtype": "Data",
"in_filter": 1,
"label": "Phone",
"no_copy": 1,
"options": "Phone"
},
{
@@ -230,7 +246,8 @@
"fieldname": "medication",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Medication"
"label": "Medication",
"no_copy": 1
},
{
"fieldname": "column_break_20",
@@ -240,13 +257,15 @@
"fieldname": "medical_history",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Medical History"
"label": "Medical History",
"no_copy": 1
},
{
"fieldname": "surgical_history",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Surgical History"
"label": "Surgical History",
"no_copy": 1
},
{
"collapsible": 1,
@@ -258,8 +277,8 @@
"fieldname": "occupation",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"in_standard_filter": 1,
"label": "Occupation"
"label": "Occupation",
"no_copy": 1
},
{
"fieldname": "column_break_25",
@@ -269,6 +288,7 @@
"fieldname": "marital_status",
"fieldtype": "Select",
"label": "Marital Status",
"no_copy": 1,
"options": "\nSingle\nMarried\nDivorced\nWidow"
},
{
@@ -281,25 +301,29 @@
"fieldname": "tobacco_past_use",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"label": "Tobacco Consumption (Past)"
"label": "Tobacco Consumption (Past)",
"no_copy": 1
},
{
"fieldname": "tobacco_current_use",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"label": "Tobacco Consumption (Present)"
"label": "Tobacco Consumption (Present)",
"no_copy": 1
},
{
"fieldname": "alcohol_past_use",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"label": "Alcohol Consumption (Past)"
"label": "Alcohol Consumption (Past)",
"no_copy": 1
},
{
"fieldname": "alcohol_current_use",
"fieldtype": "Data",
"ignore_user_permissions": 1,
"label": "Alcohol Consumption (Present)"
"label": "Alcohol Consumption (Present)",
"no_copy": 1
},
{
"fieldname": "column_break_32",
@@ -309,13 +333,15 @@
"fieldname": "surrounding_factors",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Occupational Hazards and Environmental Factors"
"label": "Occupational Hazards and Environmental Factors",
"no_copy": 1
},
{
"fieldname": "other_risk_factors",
"fieldtype": "Small Text",
"ignore_xss_filter": 1,
"label": "Other Risk Factors"
"label": "Other Risk Factors",
"no_copy": 1
},
{
"collapsible": 1,
@@ -331,7 +357,8 @@
"fieldname": "patient_details",
"fieldtype": "Text",
"ignore_xss_filter": 1,
"label": "Patient Details"
"label": "Patient Details",
"no_copy": 1
},
{
"fieldname": "default_currency",
@@ -342,19 +369,22 @@
{
"fieldname": "last_name",
"fieldtype": "Data",
"label": "Last Name"
"label": "Last Name",
"no_copy": 1
},
{
"fieldname": "first_name",
"fieldtype": "Data",
"label": "First Name",
"no_copy": 1,
"oldfieldtype": "Data",
"reqd": 1
},
{
"fieldname": "middle_name",
"fieldtype": "Data",
"label": "Middle Name (optional)"
"label": "Middle Name (optional)",
"no_copy": 1
},
{
"collapsible": 1,
@@ -389,13 +419,63 @@
"fieldtype": "Link",
"label": "Print Language",
"options": "Language"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "address_contacts",
"fieldtype": "Section Break",
"label": "Address and Contact",
"options": "fa fa-map-marker"
},
{
"fieldname": "address_html",
"fieldtype": "HTML",
"label": "Address HTML",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
{
"fieldname": "contact_html",
"fieldtype": "HTML",
"label": "Contact HTML",
"no_copy": 1,
"read_only": 1
},
{
"allow_in_quick_entry": 1,
"default": "1",
"fieldname": "invite_user",
"fieldtype": "Check",
"label": "Invite as User",
"no_copy": 1,
"read_only_depends_on": "eval: doc.user_id"
},
{
"fieldname": "user_id",
"fieldtype": "Read Only",
"label": "User ID",
"no_copy": 1,
"options": "User"
},
{
"allow_in_quick_entry": 1,
"bold": 1,
"fieldname": "uid",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Identification Number (UID)",
"unique": 1
}
],
"icon": "fa fa-user",
"image_field": "image",
"links": [],
"max_attachments": 50,
"modified": "2020-04-25 17:24:32.146415",
"modified": "2021-03-14 13:21:09.759906",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient",
@@ -453,7 +533,7 @@
],
"quick_entry": 1,
"restrict_to_domain": "Healthcare",
"search_fields": "patient_name,mobile,email,phone",
"search_fields": "patient_name,mobile,email,phone,uid",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",

View File

@@ -8,24 +8,27 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, cstr, getdate
import dateutil
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.contacts.doctype.contact.contact import get_default_contact
from frappe.model.naming import set_name_by_naming_series
from frappe.utils.nestedset import get_root_of
from erpnext import get_default_currency
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account, send_registration_sms
from erpnext.accounts.party import get_dashboard_info
class Patient(Document):
def onload(self):
'''Load address and contacts in `__onload`'''
load_address_and_contact(self)
self.load_dashboard_info()
def validate(self):
self.set_full_name()
self.add_as_website_user()
def before_insert(self):
self.set_missing_customer_details()
def after_insert(self):
self.add_as_website_user()
self.reload()
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient') and not self.customer:
create_customer(self)
if frappe.db.get_single_value('Healthcare Settings', 'collect_registration_fee'):
frappe.db.set_value('Patient', self.name, 'status', 'Disabled')
else:
@@ -50,6 +53,16 @@ class Patient(Document):
else:
create_customer(self)
self.set_contact() # add or update contact
if not self.user_id and self.email and self.invite_user:
self.create_website_user()
def load_dashboard_info(self):
if self.customer:
info = get_dashboard_info('Customer', self.customer, None)
self.set_onload('dashboard_info', info)
def set_full_name(self):
if self.last_name:
self.patient_name = ' '.join(filter(None, [self.first_name, self.last_name]))
@@ -72,18 +85,24 @@ class Patient(Document):
if not self.language:
self.language = frappe.db.get_single_value('System Settings', 'language')
def add_as_website_user(self):
if self.email:
if not frappe.db.exists ('User', self.email):
user = frappe.get_doc({
'doctype': 'User',
'first_name': self.first_name,
'last_name': self.last_name,
'email': self.email,
'user_type': 'Website User'
})
user.flags.ignore_permissions = True
user.add_roles('Patient')
def create_website_user(self):
if self.email and not frappe.db.exists('User', self.email):
user = frappe.get_doc({
'doctype': 'User',
'first_name': self.first_name,
'last_name': self.last_name,
'email': self.email,
'user_type': 'Website User',
'gender': self.sex,
'phone': self.phone,
'mobile_no': self.mobile,
'birth_date': self.dob
})
user.flags.ignore_permissions = True
user.enabled = True
user.send_welcome_email = True
user.add_roles('Patient')
frappe.db.set_value(self.doctype, self.name, 'user_id', user.name)
def autoname(self):
patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by')
@@ -108,7 +127,8 @@ class Patient(Document):
if self.dob:
dob = getdate(self.dob)
age = dateutil.relativedelta.relativedelta(getdate(), dob)
age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
age_str = f'{str(age.years)} {_("Years(s)")} {str(age.months)} {_("Month(s)")} {str(age.days)} {_("Day(s)")}'
return age_str
@frappe.whitelist()
@@ -125,6 +145,58 @@ class Patient(Document):
return {'invoice': sales_invoice.name}
def set_contact(self):
if frappe.db.exists('Dynamic Link', {'parenttype':'Contact', 'link_doctype':'Patient', 'link_name':self.name}):
old_doc = self.get_doc_before_save()
if old_doc.email != self.email or old_doc.mobile != self.mobile or old_doc.phone != self.phone:
self.update_contact()
else:
self.reload()
if self.email or self.mobile or self.phone:
contact = frappe.get_doc({
'doctype': 'Contact',
'first_name': self.first_name,
'middle_name': self.middle_name,
'last_name': self.last_name,
'gender': self.sex,
'is_primary_contact': 1
})
contact.append('links', dict(link_doctype='Patient', link_name=self.name))
if self.customer:
contact.append('links', dict(link_doctype='Customer', link_name=self.customer))
contact.insert(ignore_permissions=True)
self.update_contact(contact) # update email, mobile and phone
def update_contact(self, contact=None):
if not contact:
contact_name = get_default_contact(self.doctype, self.name)
if contact_name:
contact = frappe.get_doc('Contact', contact_name)
if contact:
if self.email and self.email != contact.email_id:
for email in contact.email_ids:
email.is_primary = True if email.email_id == self.email else False
contact.add_email(self.email, is_primary=True)
contact.set_primary_email()
if self.mobile and self.mobile != contact.mobile_no:
for mobile in contact.phone_nos:
mobile.is_primary_mobile_no = True if mobile.phone == self.mobile else False
contact.add_phone(self.mobile, is_primary_mobile_no=True)
contact.set_primary('mobile_no')
if self.phone and self.phone != contact.phone:
for phone in contact.phone_nos:
phone.is_primary_phone = True if phone.phone == self.phone else False
contact.add_phone(self.phone, is_primary_phone=True)
contact.set_primary('phone')
contact.flags.ignore_validate = True # disable hook TODO: safe?
contact.save(ignore_permissions=True)
def create_customer(doc):
customer = frappe.get_doc({
'doctype': 'Customer',
@@ -150,8 +222,8 @@ def make_invoice(patient, company):
sales_invoice.debit_to = get_receivable_account(company)
item_line = sales_invoice.append('items')
item_line.item_name = 'Registeration Fee'
item_line.description = 'Registeration Fee'
item_line.item_name = 'Registration Fee'
item_line.description = 'Registration Fee'
item_line.qty = 1
item_line.uom = uom
item_line.conversion_factor = 1
@@ -175,8 +247,11 @@ def get_patient_detail(patient):
return details
def get_timeline_data(doctype, name):
"""Return timeline data from medical records"""
return dict(frappe.db.sql('''
'''
Return Patient's timeline data from medical records
Also include the associated Customer timeline data
'''
patient_timeline_data = dict(frappe.db.sql('''
SELECT
unix_timestamp(communication_date), count(*)
FROM
@@ -185,3 +260,11 @@ def get_timeline_data(doctype, name):
patient=%s
and `communication_date` > date_sub(curdate(), interval 1 year)
GROUP BY communication_date''', name))
customer = frappe.db.get_value(doctype, name, 'customer')
if customer:
from erpnext.accounts.party import get_timeline_data
customer_timeline_data = get_timeline_data('Customer', customer)
patient_timeline_data.update(customer_timeline_data)
return patient_timeline_data

View File

@@ -6,22 +6,33 @@ def get_data():
'heatmap': True,
'heatmap_message': _('This is based on transactions against this Patient. See timeline below for details'),
'fieldname': 'patient',
'non_standard_fieldnames': {
'Payment Entry': 'party'
},
'transactions': [
{
'label': _('Appointments and Patient Encounters'),
'items': ['Patient Appointment', 'Patient Encounter']
'label': _('Appointments and Encounters'),
'items': ['Patient Appointment', 'Vital Signs', 'Patient Encounter']
},
{
'label': _('Lab Tests and Vital Signs'),
'items': ['Lab Test', 'Sample Collection', 'Vital Signs']
'items': ['Lab Test', 'Sample Collection']
},
{
'label': _('Billing'),
'items': ['Sales Invoice']
'label': _('Rehab and Physiotherapy'),
'items': ['Patient Assessment', 'Therapy Session', 'Therapy Plan']
},
{
'label': _('Orders'),
'items': ['Inpatient Medication Order']
'label': _('Surgery'),
'items': ['Clinical Procedure']
},
{
'label': _('Admissions'),
'items': ['Inpatient Record', 'Inpatient Medication Order']
},
{
'label': _('Billing and Payments'),
'items': ['Sales Invoice', 'Payment Entry']
}
]
}

View File

@@ -17,9 +17,9 @@ frappe.ui.form.on('Patient Appointment', {
},
refresh: function(frm) {
frm.set_query('patient', function () {
frm.set_query('patient', function() {
return {
filters: {'status': 'Active'}
filters: { 'status': 'Active' }
};
});
@@ -64,7 +64,7 @@ frappe.ui.form.on('Patient Appointment', {
} else {
frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd',
args: {'patient': frm.doc.patient},
args: { 'patient': frm.doc.patient },
callback: function(data) {
if (data.message == true) {
if (frm.doc.mode_of_payment && frm.doc.paid_amount) {
@@ -97,7 +97,7 @@ frappe.ui.form.on('Patient Appointment', {
if (frm.doc.patient) {
frm.add_custom_button(__('Patient History'), function() {
frappe.route_options = {'patient': frm.doc.patient};
frappe.route_options = { 'patient': frm.doc.patient };
frappe.set_route('patient_history');
}, __('View'));
}
@@ -111,14 +111,14 @@ frappe.ui.form.on('Patient Appointment', {
});
if (frm.doc.procedure_template) {
frm.add_custom_button(__('Clinical Procedure'), function(){
frm.add_custom_button(__('Clinical Procedure'), function() {
frappe.model.open_mapped_doc({
method: 'erpnext.healthcare.doctype.clinical_procedure.clinical_procedure.make_procedure',
frm: frm,
});
}, __('Create'));
} else if (frm.doc.therapy_type) {
frm.add_custom_button(__('Therapy Session'),function(){
frm.add_custom_button(__('Therapy Session'), function() {
frappe.model.open_mapped_doc({
method: 'erpnext.healthcare.doctype.therapy_session.therapy_session.create_therapy_session',
frm: frm,
@@ -148,7 +148,7 @@ frappe.ui.form.on('Patient Appointment', {
doctype: 'Patient',
name: frm.doc.patient
},
callback: function (data) {
callback: function(data) {
let age = null;
if (data.message.dob) {
age = calculate_age(data.message.dob);
@@ -165,7 +165,7 @@ frappe.ui.form.on('Patient Appointment', {
},
practitioner: function(frm) {
if (frm.doc.practitioner ) {
if (frm.doc.practitioner) {
frm.events.set_payment_details(frm);
}
},
@@ -230,7 +230,7 @@ frappe.ui.form.on('Patient Appointment', {
toggle_payment_fields: function(frm) {
frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd',
args: {'patient': frm.doc.patient},
args: { 'patient': frm.doc.patient },
callback: function(data) {
if (data.message.fee_validity) {
// if fee validity exists and automated appointment invoicing is enabled,
@@ -247,7 +247,7 @@ frappe.ui.form.on('Patient Appointment', {
frm.toggle_display('paid_amount', data.message ? 1 : 0);
frm.toggle_display('billing_item', data.message ? 1 : 0);
frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0);
frm.toggle_reqd('paid_amount', data.message ? 1 :0);
frm.toggle_reqd('paid_amount', data.message ? 1 : 0);
frm.toggle_reqd('billing_item', data.message ? 1 : 0);
}
}
@@ -258,7 +258,7 @@ frappe.ui.form.on('Patient Appointment', {
if (frm.doc.patient) {
frappe.call({
method: "erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_prescribed_therapies",
args: {patient: frm.doc.patient},
args: { patient: frm.doc.patient },
callback: function(r) {
if (r.message) {
show_therapy_types(frm, r.message);
@@ -295,13 +295,13 @@ let check_and_set_availability = function(frm) {
let d = new frappe.ui.Dialog({
title: __('Available slots'),
fields: [
{ fieldtype: 'Link', options: 'Medical Department', reqd: 1, fieldname: 'department', label: 'Medical Department'},
{ fieldtype: 'Column Break'},
{ fieldtype: 'Link', options: 'Healthcare Practitioner', reqd: 1, fieldname: 'practitioner', label: 'Healthcare Practitioner'},
{ fieldtype: 'Column Break'},
{ fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date'},
{ fieldtype: 'Section Break'},
{ fieldtype: 'HTML', fieldname: 'available_slots'}
{ fieldtype: 'Link', options: 'Medical Department', reqd: 1, fieldname: 'department', label: 'Medical Department' },
{ fieldtype: 'Column Break' },
{ fieldtype: 'Link', options: 'Healthcare Practitioner', reqd: 1, fieldname: 'practitioner', label: 'Healthcare Practitioner' },
{ fieldtype: 'Column Break' },
{ fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date' },
{ fieldtype: 'Section Break' },
{ fieldtype: 'HTML', fieldname: 'available_slots' }
],
primary_action_label: __('Book'),
@@ -379,59 +379,22 @@ let check_and_set_availability = function(frm) {
let $wrapper = d.fields_dict.available_slots.$wrapper;
// make buttons for each slot
let slot_details = data.slot_details;
let slot_html = '';
for (let i = 0; i < slot_details.length; i++) {
slot_html = slot_html + `<label>${slot_details[i].slot_name}</label>`;
slot_html = slot_html + `<br/>` + slot_details[i].avail_slot.map(slot => {
let disabled = '';
let start_str = slot.from_time;
let slot_start_time = moment(slot.from_time, 'HH:mm:ss');
let slot_to_time = moment(slot.to_time, 'HH:mm:ss');
let interval = (slot_to_time - slot_start_time)/60000 | 0;
// iterate in all booked appointments, update the start time and duration
slot_details[i].appointments.forEach(function(booked) {
let booked_moment = moment(booked.appointment_time, 'HH:mm:ss');
let end_time = booked_moment.clone().add(booked.duration, 'minutes');
// Deal with 0 duration appointments
if (booked_moment.isSame(slot_start_time) || booked_moment.isBetween(slot_start_time, slot_to_time)) {
if(booked.duration == 0){
disabled = 'disabled="disabled"';
return false;
}
}
// Check for overlaps considering appointment duration
if (slot_start_time.isBefore(end_time) && slot_to_time.isAfter(booked_moment)) {
// There is an overlap
disabled = 'disabled="disabled"';
return false;
}
});
return `<button class="btn btn-default"
data-name=${start_str}
data-duration=${interval}
data-service-unit="${slot_details[i].service_unit || ''}"
style="margin: 0 10px 10px 0; width: 72px;" ${disabled}>
${start_str.substring(0, start_str.length - 3)}
</button>`;
}).join("");
slot_html = slot_html + `<br/>`;
}
let slot_html = get_slots(data.slot_details);
$wrapper
.css('margin-bottom', 0)
.addClass('text-center')
.html(slot_html);
// blue button when clicked
// highlight button when clicked
$wrapper.on('click', 'button', function() {
let $btn = $(this);
$wrapper.find('button').removeClass('btn-primary');
$btn.addClass('btn-primary');
$wrapper.find('button').removeClass('btn-outline-primary');
$btn.addClass('btn-outline-primary');
selected_slot = $btn.attr('data-name');
service_unit = $btn.attr('data-service-unit');
duration = $btn.attr('data-duration');
// enable dialog action
// enable primary action 'Book'
d.get_primary_btn().attr('disabled', null);
});
@@ -441,19 +404,102 @@ let check_and_set_availability = function(frm) {
}
},
freeze: true,
freeze_message: __('Fetching records......')
freeze_message: __('Fetching Schedule...')
});
} else {
fd.available_slots.html(__('Appointment date and Healthcare Practitioner are Mandatory').bold());
}
}
function get_slots(slot_details) {
let slot_html = '';
let appointment_count = 0;
let disabled = false;
let start_str, slot_start_time, slot_end_time, interval, count, count_class, tool_tip, available_slots;
slot_details.forEach((slot_info) => {
slot_html += `<div class="slot-info">
<span> <b> ${__('Practitioner Schedule:')} </b> ${slot_info.slot_name} </span><br>
<span> <b> ${__('Service Unit:')} </b> ${slot_info.service_unit} </span>`;
if (slot_info.service_unit_capacity) {
slot_html += `<br><span> <b> ${__('Maximum Capacity:')} </b> ${slot_info.service_unit_capacity} </span>`;
}
slot_html += '</div><br><br>';
slot_html += slot_info.avail_slot.map(slot => {
appointment_count = 0;
disabled = false;
start_str = slot.from_time;
slot_start_time = moment(slot.from_time, 'HH:mm:ss');
slot_end_time = moment(slot.to_time, 'HH:mm:ss');
interval = (slot_end_time - slot_start_time) / 60000 | 0;
// iterate in all booked appointments, update the start time and duration
slot_info.appointments.forEach((booked) => {
let booked_moment = moment(booked.appointment_time, 'HH:mm:ss');
let end_time = booked_moment.clone().add(booked.duration, 'minutes');
// Deal with 0 duration appointments
if (booked_moment.isSame(slot_start_time) || booked_moment.isBetween(slot_start_time, slot_end_time)) {
if (booked.duration == 0) {
disabled = true;
return false;
}
}
// Check for overlaps considering appointment duration
if (slot_info.allow_overlap != 1) {
if (slot_start_time.isBefore(end_time) && slot_end_time.isAfter(booked_moment)) {
// There is an overlap
disabled = true;
return false;
}
} else {
if (slot_start_time.isBefore(end_time) && slot_end_time.isAfter(booked_moment)) {
appointment_count++;
}
if (appointment_count >= slot_info.service_unit_capacity) {
// There is an overlap
disabled = true;
return false;
}
}
});
if (slot_info.allow_overlap == 1 && slot_info.service_unit_capacity > 1) {
available_slots = slot_info.service_unit_capacity - appointment_count;
count = `${(available_slots > 0 ? available_slots : __('Full'))}`;
count_class = `${(available_slots > 0 ? 'badge-success' : 'badge-danger')}`;
tool_tip =`${available_slots} ${__('slots available for booking')}`;
}
return `
<button class="btn btn-secondary" data-name=${start_str}
data-duration=${interval}
data-service-unit="${slot_info.service_unit || ''}"
style="margin: 0 10px 10px 0; width: auto;" ${disabled ? 'disabled="disabled"' : ""}
data-toggle="tooltip" title="${tool_tip}">
${start_str.substring(0, start_str.length - 3)}<br>
<span class='badge ${count_class}'> ${count} </span>
</button>`;
}).join("");
if (slot_info.service_unit_capacity) {
slot_html += `<br/><small>${__('Each slot indicates the capacity currently available for booking')}</small>`;
}
slot_html += `<br/><br/>`;
});
return slot_html;
}
};
let get_prescribed_procedure = function(frm) {
if (frm.doc.patient) {
frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_procedure_prescribed',
args: {patient: frm.doc.patient},
args: { patient: frm.doc.patient },
callback: function(r) {
if (r.message && r.message.length) {
show_procedure_templates(frm, r.message);
@@ -473,7 +519,7 @@ let get_prescribed_procedure = function(frm) {
}
};
let show_procedure_templates = function(frm, result){
let show_procedure_templates = function(frm, result) {
let d = new frappe.ui.Dialog({
title: __('Prescribed Procedures'),
fields: [
@@ -493,9 +539,11 @@ let show_procedure_templates = function(frm, result){
data-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\
data-date="%(date)s" data-department="%(department)s">\
<button class="btn btn-default btn-xs">Add\
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {name:y[0], procedure_template: y[1],
encounter:y[2], consulting_practitioner:y[3], encounter_date:y[4],
practitioner:y[5]? y[5]:'', date: y[6]? y[6]:'', department: y[7]? y[7]:''})).appendTo(html_field);
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {
name: y[0], procedure_template: y[1],
encounter: y[2], consulting_practitioner: y[3], encounter_date: y[4],
practitioner: y[5] ? y[5] : '', date: y[6] ? y[6] : '', department: y[7] ? y[7] : ''
})).appendTo(html_field);
row.find("a").click(function() {
frm.doc.procedure_template = $(this).attr('data-procedure-template');
frm.doc.procedure_prescription = $(this).attr('data-name');
@@ -513,7 +561,7 @@ let show_procedure_templates = function(frm, result){
});
if (!result) {
let msg = __('There are no procedure prescribed for ') + frm.doc.patient;
$(repl('<div class="col-xs-12" style="padding-top:20px;" >%(msg)s</div></div>', {msg: msg})).appendTo(html_field);
$(repl('<div class="col-xs-12" style="padding-top:20px;" >%(msg)s</div></div>', { msg: msg })).appendTo(html_field);
}
d.show();
};
@@ -528,7 +576,7 @@ let show_therapy_types = function(frm, result) {
]
});
var html_field = d.fields_dict.therapy_type.$wrapper;
$.each(result, function(x, y){
$.each(result, function(x, y) {
var row = $(repl('<div class="col-xs-12" style="padding-top:12px; text-align:center;" >\
<div class="col-xs-5"> %(encounter)s <br> %(practitioner)s <br> %(date)s </div>\
<div class="col-xs-5"> %(therapy)s </div>\
@@ -537,9 +585,11 @@ let show_therapy_types = function(frm, result) {
data-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\
data-date="%(date)s" data-department="%(department)s">\
<button class="btn btn-default btn-xs">Add\
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {therapy:y[0],
name: y[1], encounter:y[2], practitioner:y[3], date:y[4],
department:y[6]? y[6]:'', therapy_plan:y[5]})).appendTo(html_field);
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {
therapy: y[0],
name: y[1], encounter: y[2], practitioner: y[3], date: y[4],
department: y[6] ? y[6] : '', therapy_plan: y[5]
})).appendTo(html_field);
row.find("a").click(function() {
frm.doc.therapy_type = $(this).attr("data-therapy");
@@ -574,13 +624,13 @@ let create_vital_signs = function(frm) {
frappe.new_doc('Vital Signs');
};
let update_status = function(frm, status){
let update_status = function(frm, status) {
let doc = frm.doc;
frappe.confirm(__('Are you sure you want to cancel this appointment?'),
function() {
frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_status',
args: {appointment_id: doc.name, status:status},
args: { appointment_id: doc.name, status: status },
callback: function(data) {
if (!data.exc) {
frm.reload_doc();

View File

@@ -131,7 +131,7 @@
"fieldtype": "Link",
"label": "Service Unit",
"options": "Healthcare Service Unit",
"set_only_once": 1
"read_only": 1
},
{
"fieldname": "section_break_12",
@@ -349,7 +349,7 @@
}
],
"links": [],
"modified": "2021-02-08 13:13:15.116833",
"modified": "2021-08-19 17:28:41.329387",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Appointment",

View File

@@ -15,6 +15,11 @@ from erpnext.hr.doctype.employee.employee import is_holiday
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account
from erpnext.healthcare.utils import check_fee_validity, get_service_item_and_practitioner_charge, manage_fee_validity
class MaximumCapacityError(frappe.ValidationError):
pass
class OverlapError(frappe.ValidationError):
pass
class PatientAppointment(Document):
def validate(self):
self.validate_overlaps()
@@ -49,26 +54,49 @@ class PatientAppointment(Document):
end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \
+ datetime.timedelta(minutes=flt(self.duration))
overlaps = frappe.db.sql("""
select
name, practitioner, patient, appointment_time, duration
from
`tabPatient Appointment`
where
appointment_date=%s and name!=%s and status NOT IN ("Closed", "Cancelled")
and (practitioner=%s or patient=%s) and
((appointment_time<%s and appointment_time + INTERVAL duration MINUTE>%s) or
(appointment_time>%s and appointment_time<%s) or
(appointment_time=%s))
""", (self.appointment_date, self.name, self.practitioner, self.patient,
self.appointment_time, end_time.time(), self.appointment_time, end_time.time(), self.appointment_time))
# all appointments for both patient and practitioner overlapping the duration of this appointment
overlapping_appointments = frappe.db.sql("""
SELECT
name, practitioner, patient, appointment_time, duration, service_unit
FROM
`tabPatient Appointment`
WHERE
appointment_date=%(appointment_date)s AND name!=%(name)s AND status NOT IN ("Closed", "Cancelled") AND
(practitioner=%(practitioner)s OR patient=%(patient)s) AND
((appointment_time<%(appointment_time)s AND appointment_time + INTERVAL duration MINUTE>%(appointment_time)s) OR
(appointment_time>%(appointment_time)s AND appointment_time<%(end_time)s) OR
(appointment_time=%(appointment_time)s))
""",
{
'appointment_date': self.appointment_date,
'name': self.name,
'practitioner': self.practitioner,
'patient': self.patient,
'appointment_time': self.appointment_time,
'end_time':end_time.time()
},
as_dict = True
)
if not overlapping_appointments:
return # No overlaps, nothing to validate!
if self.service_unit: # validate service unit capacity if overlap enabled
allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', self.service_unit,
['overlap_appointments', 'service_unit_capacity'])
if allow_overlap:
service_unit_appointments = list(filter(lambda appointment: appointment['service_unit'] == self.service_unit and
appointment['patient'] != self.patient, overlapping_appointments)) # if same patient already booked, it should be an overlap
if len(service_unit_appointments) >= (service_unit_capacity or 1):
frappe.throw(_("Not allowed, {} cannot exceed maximum capacity {}")
.format(frappe.bold(self.service_unit), frappe.bold(service_unit_capacity or 1)), MaximumCapacityError)
else: # service_unit_appointments within capacity, remove from overlapping_appointments
overlapping_appointments = [appointment for appointment in overlapping_appointments if appointment not in service_unit_appointments]
if overlapping_appointments:
frappe.throw(_("Not allowed, cannot overlap appointment {}")
.format(frappe.bold(', '.join([appointment['name'] for appointment in overlapping_appointments]))), OverlapError)
if overlaps:
overlapping_details = _('Appointment overlaps with ')
overlapping_details += "<b><a href='/app/Form/Patient Appointment/{0}'>{0}</a></b><br>".format(overlaps[0][0])
overlapping_details += _('{0} has appointment scheduled with {1} at {2} having {3} minute(s) duration.').format(
overlaps[0][1], overlaps[0][2], overlaps[0][3], overlaps[0][4])
frappe.throw(overlapping_details, title=_('Appointments Overlapping'))
def validate_service_unit(self):
if self.inpatient_record and self.service_unit:
@@ -327,6 +355,8 @@ def get_available_slots(practitioner_doc, date):
if available_slots:
appointments = []
allow_overlap = 0
service_unit_capacity = 0
# fetch all appointments to practitioner by service unit
filters = {
'practitioner': practitioner,
@@ -336,8 +366,8 @@ def get_available_slots(practitioner_doc, date):
}
if schedule_entry.service_unit:
slot_name = schedule_entry.schedule + ' - ' + schedule_entry.service_unit
allow_overlap = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, 'overlap_appointments')
slot_name = f'{schedule_entry.schedule}'
allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, ['overlap_appointments', 'service_unit_capacity'])
if not allow_overlap:
# fetch all appointments to service unit
filters.pop('practitioner')
@@ -352,8 +382,8 @@ def get_available_slots(practitioner_doc, date):
filters=filters,
fields=['name', 'appointment_time', 'duration', 'status'])
slot_details.append({'slot_name':slot_name, 'service_unit':schedule_entry.service_unit,
'avail_slot':available_slots, 'appointments': appointments})
slot_details.append({'slot_name': slot_name, 'service_unit': schedule_entry.service_unit, 'avail_slot': available_slots,
'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity})
return slot_details

View File

@@ -15,9 +15,11 @@ class TestPatientAppointment(unittest.TestCase):
frappe.db.sql("""delete from `tabFee Validity`""")
frappe.db.sql("""delete from `tabPatient Encounter`""")
make_pos_profile()
frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test %'""")
frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test Service Unit Type%'""")
def test_status(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
appointment = create_appointment(patient, practitioner, nowdate())
self.assertEqual(appointment.status, 'Open')
@@ -29,7 +31,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
def test_start_encounter(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1)
appointment.reload()
@@ -43,7 +45,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'))
def test_auto_invoicing(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
appointment = create_appointment(patient, practitioner, nowdate())
@@ -59,7 +61,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
def test_auto_invoicing_based_on_department(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment_type = create_appointment_type()
@@ -77,7 +79,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
def test_auto_invoicing_according_to_appointment_type_charge(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
@@ -103,7 +105,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertTrue(sales_invoice_name)
def test_appointment_cancel(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
appointment = create_appointment(patient, practitioner, nowdate())
fee_validity = frappe.db.get_value('Fee Validity Reference', {'appointment': appointment.name}, 'parent')
@@ -129,7 +131,7 @@ class TestPatientAppointment(unittest.TestCase):
create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
@@ -137,7 +139,7 @@ class TestPatientAppointment(unittest.TestCase):
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit)
@@ -155,7 +157,7 @@ class TestPatientAppointment(unittest.TestCase):
create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
@@ -163,10 +165,10 @@ class TestPatientAppointment(unittest.TestCase):
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
appointment_service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy for Appointment')
appointment_service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy for Appointment')
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0)
self.assertRaises(frappe.exceptions.ValidationError, appointment.save)
@@ -176,39 +178,102 @@ class TestPatientAppointment(unittest.TestCase):
mark_invoiced_inpatient_occupancy(ip_record1)
discharge_patient(ip_record1)
def test_overlap_appointment(self):
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import OverlapError
patient, practitioner = create_healthcare_docs(id=1)
patient_1, practitioner_1 = create_healthcare_docs(id=2)
service_unit = create_service_unit(id=0)
service_unit_1 = create_service_unit(id=1)
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit) # valid
def create_healthcare_docs():
patient = create_patient()
practitioner = frappe.db.exists('Healthcare Practitioner', '_Test Healthcare Practitioner')
medical_department = frappe.db.exists('Medical Department', '_Test Medical Department')
# patient and practitioner cannot have overlapping appointments
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit_1, save=0) # diff service unit
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner, nowdate(), save=0) # with no service unit link
self.assertRaises(OverlapError, appointment.save)
if not medical_department:
medical_department = frappe.new_doc('Medical Department')
medical_department.department = '_Test Medical Department'
medical_department.save(ignore_permissions=True)
medical_department = medical_department.name
# patient cannot have overlapping appointments with other practitioners
appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit_1, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner_1, nowdate(), save=0)
self.assertRaises(OverlapError, appointment.save)
if not practitioner:
practitioner = frappe.new_doc('Healthcare Practitioner')
practitioner.first_name = '_Test Healthcare Practitioner'
practitioner.gender = 'Female'
practitioner.department = medical_department
practitioner.op_consulting_charge = 500
practitioner.inpatient_visit_charge = 500
practitioner.save(ignore_permissions=True)
practitioner = practitioner.name
# practitioner cannot have overlapping appointments with other patients
appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit_1, save=0)
self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient_1, practitioner, nowdate(), save=0)
self.assertRaises(OverlapError, appointment.save)
return patient, medical_department, practitioner
def test_service_unit_capacity(self):
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import MaximumCapacityError, OverlapError
practitioner = create_practitioner()
capacity = 3
overlap_service_unit_type = create_service_unit_type(id=10, allow_appointments=1, overlap_appointments=1)
overlap_service_unit = create_service_unit(id=100, service_unit_type=overlap_service_unit_type, service_unit_capacity=capacity)
for i in range(0, capacity):
patient = create_patient(id=i)
create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit) # valid
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0) # overlap
self.assertRaises(OverlapError, appointment.save)
patient = create_patient(id=capacity)
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0)
self.assertRaises(MaximumCapacityError, appointment.save)
def create_healthcare_docs(id=0):
patient = create_patient(id)
practitioner = create_practitioner(id)
return patient, practitioner
def create_patient(id=0):
if frappe.db.exists('Patient', {'firstname':f'_Test Patient {str(id)}'}):
patient = frappe.db.get_value('Patient', {'first_name': f'_Test Patient {str(id)}'}, ['name'])
return patient
patient = frappe.new_doc('Patient')
patient.first_name = f'_Test Patient {str(id)}'
patient.sex = 'Female'
patient.save(ignore_permissions=True)
return patient.name
def create_medical_department(id=0):
if frappe.db.exists('Medical Department', f'_Test Medical Department {str(id)}'):
return f'_Test Medical Department {str(id)}'
medical_department = frappe.new_doc('Medical Department')
medical_department.department = f'_Test Medical Department {str(id)}'
medical_department.save(ignore_permissions=True)
return medical_department.name
def create_practitioner(id=0, medical_department=None):
if frappe.db.exists('Healthcare Practitioner', {'firstname':f'_Test Healthcare Practitioner {str(id)}'}):
practitioner = frappe.db.get_value('Healthcare Practitioner', {'firstname':f'_Test Healthcare Practitioner {str(id)}'}, ['name'])
return practitioner
practitioner = frappe.new_doc('Healthcare Practitioner')
practitioner.first_name = f'_Test Healthcare Practitioner {str(id)}'
practitioner.gender = 'Female'
practitioner.department = medical_department or create_medical_department(id)
practitioner.op_consulting_charge = 500
practitioner.inpatient_visit_charge = 500
practitioner.save(ignore_permissions=True)
return practitioner.name
def create_patient():
patient = frappe.db.exists('Patient', '_Test Patient')
if not patient:
patient = frappe.new_doc('Patient')
patient.first_name = '_Test Patient'
patient.sex = 'Female'
patient.save(ignore_permissions=True)
patient = patient.name
return patient
def create_encounter(appointment):
if appointment:
@@ -221,8 +286,10 @@ def create_encounter(appointment):
encounter.company = appointment.company
encounter.save()
encounter.submit()
return encounter
def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0,
service_unit=None, appointment_type=None, save=1, department=None):
item = create_healthcare_service_items()
@@ -235,6 +302,7 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.appointment_date = appointment_date
appointment.company = '_Test Company'
appointment.duration = 15
if service_unit:
appointment.service_unit = service_unit
if invoice:
@@ -245,11 +313,14 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.procedure_template = create_clinical_procedure_template().get('name')
if save:
appointment.save(ignore_permissions=True)
return appointment
def create_healthcare_service_items():
if frappe.db.exists('Item', 'HLC-SI-001'):
return 'HLC-SI-001'
item = frappe.new_doc('Item')
item.item_code = 'HLC-SI-001'
item.item_name = 'Consulting Charges'
@@ -257,11 +328,14 @@ def create_healthcare_service_items():
item.is_stock_item = 0
item.stock_uom = 'Nos'
item.save()
return item.name
def create_clinical_procedure_template():
if frappe.db.exists('Clinical Procedure Template', 'Knee Surgery and Rehab'):
return frappe.get_doc('Clinical Procedure Template', 'Knee Surgery and Rehab')
template = frappe.new_doc('Clinical Procedure Template')
template.template = 'Knee Surgery and Rehab'
template.item_code = 'Knee Surgery and Rehab'
@@ -270,8 +344,10 @@ def create_clinical_procedure_template():
template.description = 'Knee Surgery and Rehab'
template.rate = 50000
template.save()
return template
def create_appointment_type(args=None):
if not args:
args = frappe.local.form_dict
@@ -296,3 +372,28 @@ def create_appointment_type(args=None):
'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}),
'items': args.get('items') or items
}).insert()
def create_service_unit_type(id=0, allow_appointments=1, overlap_appointments=0):
if frappe.db.exists('Healthcare Service Unit Type', f'_Test Service Unit Type {str(id)}'):
return f'_Test Service Unit Type {str(id)}'
service_unit_type = frappe.new_doc('Healthcare Service Unit Type')
service_unit_type.service_unit_type = f'_Test Service Unit Type {str(id)}'
service_unit_type.allow_appointments = allow_appointments
service_unit_type.overlap_appointments = overlap_appointments
service_unit_type.save(ignore_permissions=True)
return service_unit_type.name
def create_service_unit(id=0, service_unit_type=None, service_unit_capacity=0):
if frappe.db.exists('Healthcare Service Unit', f'_Test Service Unit {str(id)}'):
return f'_Test service_unit {str(id)}'
service_unit = frappe.new_doc('Healthcare Service Unit')
service_unit.is_group = 0
service_unit.healthcare_service_unit_name= f'_Test Service Unit {str(id)}'
service_unit.service_unit_type = service_unit_type or create_service_unit_type(id)
service_unit.service_unit_capacity = service_unit_capacity
service_unit.save(ignore_permissions=True)
return service_unit.name

View File

@@ -31,6 +31,3 @@ def create_patient_assessment(source_name, target_doc=None):
}, target_doc)
return doc

View File

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import nowdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment, create_medical_department
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
class TestPatientMedicalRecord(unittest.TestCase):
@@ -15,7 +15,8 @@ class TestPatientMedicalRecord(unittest.TestCase):
make_pos_profile()
def test_medical_record(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
medical_department = create_medical_department()
appointment = create_appointment(patient, practitioner, nowdate(), invoice=1)
encounter = create_encounter(appointment)

View File

@@ -8,11 +8,13 @@ import unittest
from frappe.utils import getdate, flt, nowdate
from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type
from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient, create_appointment
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import \
create_healthcare_docs, create_patient, create_appointment, create_medical_department
class TestTherapyPlan(unittest.TestCase):
def test_creation_on_encounter_submission(self):
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
medical_department = create_medical_department()
encounter = create_encounter(patient, medical_department, practitioner)
self.assertTrue(frappe.db.exists('Therapy Plan', encounter.therapy_plan))
@@ -28,8 +30,9 @@ class TestTherapyPlan(unittest.TestCase):
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
patient, medical_department, practitioner = create_healthcare_docs()
patient, practitioner = create_healthcare_docs()
appointment = create_appointment(patient, practitioner, nowdate())
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
session = frappe.get_doc(session)
session.submit()

View File

@@ -34,7 +34,8 @@ def create_therapy_type():
})
therapy_type.save()
else:
therapy_type = frappe.get_doc('Therapy Type', 'Basic Rehab')
therapy_type = frappe.get_doc('Therapy Type', therapy_type)
return therapy_type
def create_exercise_type():
@@ -47,4 +48,7 @@ def create_exercise_type():
'description': 'Squat and Rise'
})
exercise_type.save()
else:
exercise_type = frappe.get_doc('Exercise Type', exercise_type)
return exercise_type

View File

@@ -15,4 +15,3 @@ class VitalSigns(Document):
def set_title(self):
self.title = _('{0} on {1}').format(self.patient_name or self.patient,
frappe.utils.format_date(self.signs_date))[:100]

View File

@@ -194,4 +194,3 @@ def get_date_range(time_span):
return time_span
except json.decoder.JSONDecodeError:
return get_timespan_date_range(time_span.lower())

View File

@@ -25,7 +25,7 @@ class TestInpatientMedicationOrders(unittest.TestCase):
'from_date': getdate(),
'to_date': getdate(),
'patient': '_Test IPD Patient',
'service_unit': 'Test Service Unit Ip Occupancy - _TC'
'service_unit': '_Test Service Unit Ip Occupancy - _TC'
}
report = execute(filters)
@@ -42,7 +42,7 @@ class TestInpatientMedicationOrders(unittest.TestCase):
'date': getdate(),
'time': datetime.timedelta(seconds=32400),
'is_completed': 0,
'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC'
},
{
'patient': '_Test IPD Patient',
@@ -55,7 +55,7 @@ class TestInpatientMedicationOrders(unittest.TestCase):
'date': getdate(),
'time': datetime.timedelta(seconds=50400),
'is_completed': 0,
'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC'
},
{
'patient': '_Test IPD Patient',
@@ -68,7 +68,7 @@ class TestInpatientMedicationOrders(unittest.TestCase):
'date': getdate(),
'time': datetime.timedelta(seconds=75600),
'is_completed': 0,
'healthcare_service_unit': 'Test Service Unit Ip Occupancy - _TC'
'healthcare_service_unit': '_Test Service Unit Ip Occupancy - _TC'
}
]
@@ -83,7 +83,7 @@ class TestInpatientMedicationOrders(unittest.TestCase):
'from_date': getdate(),
'to_date': getdate(),
'patient': '_Test IPD Patient',
'service_unit': 'Test Service Unit Ip Occupancy - _TC',
'service_unit': '_Test Service Unit Ip Occupancy - _TC',
'show_completed_orders': 0
}
@@ -119,7 +119,7 @@ def create_records(patient):
ip_record.expected_length_of_stay = 0
ip_record.save()
ip_record.reload()
service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
ipmo = create_ipmo(patient)

View File

@@ -543,58 +543,43 @@ def get_drugs_to_invoice(encounter):
@frappe.whitelist()
def get_children(doctype, parent, company, is_root=False):
parent_fieldname = "parent_" + doctype.lower().replace(" ", "_")
def get_children(doctype, parent=None, company=None, is_root=False):
parent_fieldname = 'parent_' + doctype.lower().replace(' ', '_')
fields = [
"name as value",
"is_group as expandable",
"lft",
"rgt"
'name as value',
'is_group as expandable',
'lft',
'rgt'
]
# fields = [ "name", "is_group", "lft", "rgt" ]
filters = [["ifnull(`{0}`,'')".format(parent_fieldname), "=", "" if is_root else parent]]
filters = [["ifnull(`{0}`,'')".format(parent_fieldname),
'=', '' if is_root else parent]]
if is_root:
fields += ["service_unit_type"] if doctype == "Healthcare Service Unit" else []
filters.append(["company", "=", company])
fields += ['service_unit_type'] if doctype == 'Healthcare Service Unit' else []
filters.append(['company', '=', company])
else:
fields += ["service_unit_type", "allow_appointments", "inpatient_occupancy", "occupancy_status"] if doctype == "Healthcare Service Unit" else []
fields += [parent_fieldname + " as parent"]
fields += ['service_unit_type', 'allow_appointments', 'inpatient_occupancy',
'occupancy_status'] if doctype == 'Healthcare Service Unit' else []
fields += [parent_fieldname + ' as parent']
hc_service_units = frappe.get_list(doctype, fields=fields, filters=filters)
service_units = frappe.get_list(doctype, fields=fields, filters=filters)
for each in service_units:
if each['expandable'] == 1: # group node
available_count = frappe.db.count('Healthcare Service Unit', filters={
'parent_healthcare_service_unit': each['value'],
'inpatient_occupancy': 1})
if doctype == "Healthcare Service Unit":
for each in hc_service_units:
occupancy_msg = ""
if each["expandable"] == 1:
occupied = False
vacant = False
child_list = frappe.db.sql(
'''
SELECT
name, occupancy_status
FROM
`tabHealthcare Service Unit`
WHERE
inpatient_occupancy = 1
and lft > %s and rgt < %s
''', (each['lft'], each['rgt']))
if available_count > 0:
occupied_count = frappe.db.count('Healthcare Service Unit', {
'parent_healthcare_service_unit': each['value'],
'inpatient_occupancy': 1,
'occupancy_status': 'Occupied'})
# set occupancy status of group node
each['occupied_of_available'] = str(
occupied_count) + ' Occupied of ' + str(available_count)
for child in child_list:
if not occupied:
occupied = 0
if child[1] == "Occupied":
occupied += 1
if not vacant:
vacant = 0
if child[1] == "Vacant":
vacant += 1
if vacant and occupied:
occupancy_total = vacant + occupied
occupancy_msg = str(occupied) + " Occupied out of " + str(occupancy_total)
each["occupied_out_of_vacant"] = occupancy_msg
return hc_service_units
return service_units
@frappe.whitelist()
@@ -717,3 +702,40 @@ def render_doc_as_html(doctype, docname, exclude_fields = []):
doc_html = "<div class='small'><div class='col-md-12 text-right'><a class='btn btn-default btn-xs' href='/app/Form/%s/%s'></a></div>" %(doctype, docname) + doc_html + '</div>'
return {'html': doc_html}
def update_address_links(address, method):
'''
Hook validate Address
If Patient is linked in Address, also link the associated Customer
'''
if 'Healthcare' not in frappe.get_active_domains():
return
patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', address.links))
for link in patient_links:
customer = frappe.db.get_value('Patient', link.get('link_name'), 'customer')
if customer and not address.has_link('Customer', customer):
address.append('links', dict(link_doctype = 'Customer', link_name = customer))
def update_patient_email_and_phone_numbers(contact, method):
'''
Hook validate Contact
Update linked Patients' primary mobile and phone numbers
'''
if 'Healthcare' not in frappe.get_active_domains():
return
if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone):
patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', contact.links))
for link in patient_links:
contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1)
if contact.email_id and contact.email_id != contact_details.get('email'):
frappe.db.set_value('Patient', link.get('link_name'), 'email', contact.email_id)
if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'):
frappe.db.set_value('Patient', link.get('link_name'), 'mobile', contact.mobile_no)
if contact.phone and contact.phone != contact_details.get('phone'):
frappe.db.set_value('Patient', link.get('link_name'), 'phone', contact.phone)

View File

@@ -282,7 +282,12 @@ doc_events = {
"on_trash": "erpnext.regional.check_deletion_permission"
},
'Address': {
'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code', 'erpnext.regional.india.utils.update_gst_category']
'validate': [
'erpnext.regional.india.utils.validate_gstin_for_india',
'erpnext.regional.italy.utils.set_state_code',
'erpnext.regional.india.utils.update_gst_category',
'erpnext.healthcare.utils.update_address_links'
],
},
'Supplier': {
'validate': 'erpnext.regional.india.utils.validate_pan_for_india'
@@ -293,13 +298,16 @@ doc_events = {
"Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",
"validate": "erpnext.crm.utils.update_lead_phone_numbers"
"validate": ["erpnext.crm.utils.update_lead_phone_numbers", "erpnext.healthcare.utils.update_patient_email_and_phone_numbers"]
},
"Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
},
('Quotation', 'Sales Order', 'Sales Invoice'): {
'validate': ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"]
},
"Company": {
"on_trash": "erpnext.regional.india.utils.delete_gst_settings_for_company"
}
}

View File

@@ -55,4 +55,3 @@ QUnit.test("Test: Expense Claim [HR]", function (assert) {
() => done()
]);
});

View File

@@ -27,4 +27,3 @@ QUnit.test("Test: Appraisal Template [HR]", function (assert) {
() => done()
]);
});

View File

@@ -267,5 +267,3 @@ erpnext.EmployeeSelector = Class.extend({
mark_employee_toolbar.appendTo($(this.wrapper));
}
});

View File

@@ -176,4 +176,3 @@ def time_diff_in_hours(start, end):
def find_index_in_dict(dict_list, key, value):
return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None)

View File

@@ -12,4 +12,3 @@ class EmployeeGrievance(Document):
bold("Invalid"),
bold("Resolved"))
)

View File

@@ -48,4 +48,3 @@ def create_grievance_type():
grievance_type.save()
return grievance_type.name

Some files were not shown because too many files have changed in this diff Show More