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 # This commit just changes spaces to tabs for indentation in some files
5f473611bd6ed57703716244a054d3fb5ba9cd23 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 return debit_account
else: else:
return credit_account return credit_account

View File

@@ -113,5 +113,3 @@ def disable_dimension():
dimension2 = frappe.get_doc("Accounting Dimension", "Location") dimension2 = frappe.get_doc("Accounting Dimension", "Location")
dimension2.disabled = 1 dimension2.disabled = 1
dimension2.save() 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) frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
return doc.payment_entry 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.'), frappe._('You can only select a maximum of one option from the list of check boxes.'),
title='Error' title='Error'
) )

View File

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

View File

@@ -124,6 +124,3 @@ class TestCouponCode(unittest.TestCase):
so.submit() so.submit()
self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1) 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 \ message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \
Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode." Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode."
frappe.throw(_(message), title="Not Allowed") 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")) frappe.throw(_("Please add a Temporary Opening account in Chart of Accounts"))
return accounts[0].name return accounts[0].name

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -630,5 +630,3 @@ class TestSubscription(unittest.TestCase):
subscription.process() subscription.process()
self.assertEqual(len(subscription.invoices), 1) 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) .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
def validate_party_accounts(doc): def validate_party_accounts(doc):
companies = [] companies = []
for account in doc.get("accounts"): for account in doc.get("accounts"):
@@ -446,6 +447,10 @@ def get_payment_terms_template(party_name, party_type, company=None):
return template return template
def validate_party_frozen_disabled(party_type, party_name): 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 and party_name:
if party_type in ("Customer", "Supplier"): if party_type in ("Customer", "Supplier"):
party = frappe.get_cached_value(party_type, party_name, ["is_frozen", "disabled"], as_dict=True) party = frappe.get_cached_value(party_type, party_name, ["is_frozen", "disabled"], as_dict=True)

View File

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

View File

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

View File

@@ -136,4 +136,3 @@ frappe.query_reports["Accounts Payable"] = {
} }
erpnext.utils.add_dimensions('Accounts Payable', 9); 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); 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"], "naming_by": ["Buying Settings", "supp_master_name"],
} }
return AccountsReceivableSummary(filters).run(args) return AccountsReceivableSummary(filters).run(args)

View File

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

View File

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

View File

@@ -92,4 +92,3 @@ frappe.query_reports["Budget Variance Report"] = {
erpnext.dimension_filters.forEach((dimension) => { erpnext.dimension_filters.forEach((dimension) => {
frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]); 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' 'type' : 'bar'
} }

View File

@@ -176,4 +176,3 @@ frappe.query_reports["General Ledger"] = {
} }
erpnext.utils.add_dimensions('General Ledger', 15) 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, 'company': d.company,
'sales_order': d.sales_order, 'sales_order': d.sales_order,
'delivery_note': d.delivery_note, '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, 'cost_center': d.cost_center,
'stock_qty': d.stock_qty, 'stock_qty': d.stock_qty,
'stock_uom': d.stock_uom '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 Item`.name, `tabSales Invoice Item`.parent,
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
`tabSales Invoice`.unrealized_profit_loss_account, `tabSales Invoice`.unrealized_profit_loss_account,
`tabSales Invoice`.is_internal_customer,
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, `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: for tax in tax_columns:
total_row.setdefault(frappe.scrub(tax + ' Amount'), 0.0) total_row.setdefault(frappe.scrub(tax + ' Amount'), 0.0)
total_row[frappe.scrub(tax + ' Amount')] += flt(item[frappe.scrub(tax + ' Amount')]) 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); 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 # Add amount in unrealized account
for account in unrealized_profit_loss_accounts: for account in unrealized_profit_loss_accounts:
row.update({ 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 # 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 unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabSales Invoice` where docstatus = 1 and name in (%s) from `tabSales Invoice` where docstatus = 1 and name in (%s)
and is_internal_customer = 1
and ifnull(unrealized_profit_loss_account, '') != '' and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account""" % order by unrealized_profit_loss_account""" %
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) ', '.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: for account in unrealized_profit_loss_accounts:
unrealized_profit_loss_account_columns.append({ unrealized_profit_loss_account_columns.append({
"label": account, "label": account,
"fieldname": frappe.scrub(account), "fieldname": frappe.scrub(account+"_unrealized"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120 "width": 120

View File

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

View File

@@ -351,6 +351,7 @@ def reconcile_against_document(args):
# cancel advance entry # cancel advance entry
doc = frappe.get_doc(d.voucher_type, d.voucher_no) 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) doc.make_gl_entries(cancel=1, adv_adj=1)
# update ref in advance entry # update ref in advance entry
@@ -362,6 +363,7 @@ def reconcile_against_document(args):
# re-submit advance entry # re-submit advance entry
doc = frappe.get_doc(d.voucher_type, d.voucher_no) doc = frappe.get_doc(d.voucher_type, d.voucher_no)
doc.make_gl_entries(cancel = 0, adv_adj =1) doc.make_gl_entries(cancel = 0, adv_adj =1)
frappe.flags.ignore_party_validation = False
if d.voucher_type in ('Payment Entry', 'Journal Entry'): if d.voucher_type in ('Payment Entry', 'Journal Entry'):
doc.update_expense_claim() 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) )" "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) }, target_doc, post_process, ignore_permissions=True)
return doc return doc

View File

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

View File

@@ -102,4 +102,3 @@ def get_linked_material_requests(items):
mr_list.append(material_request) mr_list.append(material_request)
return mr_list 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 qty_unchanged = prev_qty == new_qty
uom_unchanged = prev_uom == new_uom uom_unchanged = prev_uom == new_uom
conversion_factor_unchanged = prev_con_fac == new_con_fac 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: if rate_unchanged and qty_unchanged and conversion_factor_unchanged and uom_unchanged and date_unchanged:
continue continue

View File

@@ -344,4 +344,3 @@ def create_variant_doc_for_quick_entry(template, args):
variant.name = variant.item_code variant.name = variant.item_code
validate_item_variant_attributes(variant, args) validate_item_variant_attributes(variant, args)
return variant.as_dict() 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: if meta.is_tree:
query_filters.append(['is_group', '=', 0]) query_filters.append(['is_group', '=', 0])
if meta.has_field('disabled'):
query_filters.append(['disabled', '!=', 1])
if meta.has_field('company'): if meta.has_field('company'):
query_filters.append(['company', '=', filters.get('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 # frappe.db.exists returns a tuple of a tuple
return frappe.get_doc('Employee', employee_docname[0][0]) return frappe.get_doc('Employee', employee_docname[0][0])
return None 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") conditions.append(" and `tabLead`.status=%(status)s")
return " ".join(conditions) if conditions else "" 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)}) "student":self.student, "assessment_plan":self.assessment_plan, "docstatus":("!=", 2)})
if assessment_result: if assessment_result:
frappe.throw(_("Assessment Result record {0} already exists.").format(getlink("Assessment Result",assessment_result[0].name))) 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) grade = get_grade("_Test Grading Scale", 70)
self.assertEqual("B", grade) self.assertEqual("B", grade)

View File

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

View File

@@ -47,4 +47,3 @@ class CourseSchedule(Document):
validate_overlap_for(self, "Assessment Plan", "room") validate_overlap_for(self, "Assessment Plan", "room")
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor) 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] 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 order by idx desc, name
limit %s, %s""".format(searchfield), limit %s, %s""".format(searchfield),
tuple(["%%%s%%" % txt, "%%%s%%" % txt, start, page_len])) tuple(["%%%s%%" % txt, "%%%s%%" % txt, start, page_len]))

View File

@@ -121,4 +121,3 @@ def get_chart_data(data):
}, },
'type': 'bar' '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')) return getdate(shopify_settings.to_date) < getdate(order.get('created_at'))
else: else:
return cstr(order.get('id')) == cstr(shopify_settings.to_order_id) 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 // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt

View File

@@ -11,7 +11,7 @@ test_dependencies = ['Item']
class TestClinicalProcedure(unittest.TestCase): class TestClinicalProcedure(unittest.TestCase):
def test_procedure_template_item(self): def test_procedure_template_item(self):
patient, medical_department, practitioner = create_healthcare_docs() patient, practitioner = create_healthcare_docs()
procedure_template = create_clinical_procedure_template() procedure_template = create_clinical_procedure_template()
self.assertTrue(frappe.db.exists('Item', procedure_template.item)) 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) self.assertEqual(frappe.db.get_value('Item', procedure_template.item, 'disabled'), 1)
def test_consumables(self): def test_consumables(self):
patient, medical_department, practitioner = create_healthcare_docs() patient, practitioner = create_healthcare_docs()
procedure_template = create_clinical_procedure_template() procedure_template = create_clinical_procedure_template()
procedure_template.allow_stock_consumption = 1 procedure_template.allow_stock_consumption = 1
consumable = create_consumable() 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.') 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) rename_doc('Item', doc.item_code, item_code, ignore_permissions=True)
frappe.db.set_value('Clinical Procedure Template', doc.name, 'item_code', item_code) frappe.db.set_value('Clinical Procedure Template', doc.name, 'item_code', item_code)
return return

View File

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

View File

@@ -27,7 +27,7 @@ class TestFeeValidity(unittest.TestCase):
healthcare_settings.automate_appointment_invoicing = 1 healthcare_settings.automate_appointment_invoicing = 1
healthcare_settings.op_consulting_charge_item = item healthcare_settings.op_consulting_charge_item = item
healthcare_settings.save(ignore_permissions=True) 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 should not be invoiced. Check Fee Validity created for new patient
appointment = create_appointment(patient, practitioner, nowdate()) 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.') 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 // get query select healthcare service unit
frm.fields_dict['parent_healthcare_service_unit'].get_query = function(doc) { frm.fields_dict['parent_healthcare_service_unit'].get_query = function(doc) {
return{ return {
filters:[ filters: [
['Healthcare Service Unit', 'is_group', '=', 1], ['Healthcare Service Unit', 'is_group', '=', 1],
['Healthcare Service Unit', 'name', '!=', doc.healthcare_service_unit_name] ['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() { frm.add_custom_button(__('Healthcare Service Unit Tree'), function() {
frappe.set_route('Tree', 'Healthcare Service Unit'); frappe.set_route('Tree', 'Healthcare Service Unit');
}); });
frm.set_query('warehouse', function() {
return {
filters: {
'company': frm.doc.company
}
};
});
}, },
set_root_readonly: function(frm) { set_root_readonly: function(frm) {
// read-only for root healthcare service unit // read-only for root healthcare service unit
@@ -43,5 +51,10 @@ frappe.ui.form.on('Healthcare Service Unit', {
else { else {
frm.set_df_property('service_unit_type', 'reqd', 1); 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", "service_unit_type",
"allow_appointments", "allow_appointments",
"overlap_appointments", "overlap_appointments",
"service_unit_capacity",
"inpatient_occupancy", "inpatient_occupancy",
"occupancy_status", "occupancy_status",
"column_break_9", "column_break_9",
@@ -31,6 +32,8 @@
{ {
"fieldname": "healthcare_service_unit_name", "fieldname": "healthcare_service_unit_name",
"fieldtype": "Data", "fieldtype": "Data",
"hide_days": 1,
"hide_seconds": 1,
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Service Unit", "label": "Service Unit",
@@ -41,6 +44,8 @@
"bold": 1, "bold": 1,
"fieldname": "parent_healthcare_service_unit", "fieldname": "parent_healthcare_service_unit",
"fieldtype": "Link", "fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Parent Service Unit", "label": "Parent Service Unit",
@@ -52,6 +57,8 @@
"depends_on": "eval:doc.inpatient_occupancy != 1 && doc.allow_appointments != 1", "depends_on": "eval:doc.inpatient_occupancy != 1 && doc.allow_appointments != 1",
"fieldname": "is_group", "fieldname": "is_group",
"fieldtype": "Check", "fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Is Group" "label": "Is Group"
}, },
{ {
@@ -59,6 +66,8 @@
"depends_on": "eval:doc.is_group != 1", "depends_on": "eval:doc.is_group != 1",
"fieldname": "service_unit_type", "fieldname": "service_unit_type",
"fieldtype": "Link", "fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Service Unit Type", "label": "Service Unit Type",
"options": "Healthcare Service Unit Type" "options": "Healthcare Service Unit Type"
}, },
@@ -68,6 +77,8 @@
"fetch_from": "service_unit_type.allow_appointments", "fetch_from": "service_unit_type.allow_appointments",
"fieldname": "allow_appointments", "fieldname": "allow_appointments",
"fieldtype": "Check", "fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Allow Appointments", "label": "Allow Appointments",
"no_copy": 1, "no_copy": 1,
@@ -79,6 +90,8 @@
"fetch_from": "service_unit_type.overlap_appointments", "fetch_from": "service_unit_type.overlap_appointments",
"fieldname": "overlap_appointments", "fieldname": "overlap_appointments",
"fieldtype": "Check", "fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"label": "Allow Overlap", "label": "Allow Overlap",
"no_copy": 1, "no_copy": 1,
"read_only": 1 "read_only": 1
@@ -90,6 +103,8 @@
"fetch_from": "service_unit_type.inpatient_occupancy", "fetch_from": "service_unit_type.inpatient_occupancy",
"fieldname": "inpatient_occupancy", "fieldname": "inpatient_occupancy",
"fieldtype": "Check", "fieldtype": "Check",
"hide_days": 1,
"hide_seconds": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Inpatient Occupancy", "label": "Inpatient Occupancy",
"no_copy": 1, "no_copy": 1,
@@ -100,6 +115,8 @@
"depends_on": "eval:doc.inpatient_occupancy == 1", "depends_on": "eval:doc.inpatient_occupancy == 1",
"fieldname": "occupancy_status", "fieldname": "occupancy_status",
"fieldtype": "Select", "fieldtype": "Select",
"hide_days": 1,
"hide_seconds": 1,
"label": "Occupancy Status", "label": "Occupancy Status",
"no_copy": 1, "no_copy": 1,
"options": "Vacant\nOccupied", "options": "Vacant\nOccupied",
@@ -107,13 +124,17 @@
}, },
{ {
"fieldname": "column_break_9", "fieldname": "column_break_9",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1
}, },
{ {
"bold": 1, "bold": 1,
"depends_on": "eval:doc.is_group != 1", "depends_on": "eval:doc.is_group != 1",
"fieldname": "warehouse", "fieldname": "warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Warehouse", "label": "Warehouse",
"no_copy": 1, "no_copy": 1,
"options": "Warehouse" "options": "Warehouse"
@@ -121,6 +142,8 @@
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
@@ -134,6 +157,8 @@
"fieldname": "lft", "fieldname": "lft",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 1, "hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "lft", "label": "lft",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
@@ -143,6 +168,8 @@
"fieldname": "rgt", "fieldname": "rgt",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 1, "hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "rgt", "label": "rgt",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
@@ -152,6 +179,8 @@
"fieldname": "old_parent", "fieldname": "old_parent",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"label": "Old Parent", "label": "Old Parent",
"no_copy": 1, "no_copy": 1,
@@ -163,14 +192,26 @@
"collapsible": 1, "collapsible": 1,
"fieldname": "tree_details_section", "fieldname": "tree_details_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Tree Details" "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": [], "links": [],
"modified": "2020-05-20 18:26:56.065543", "modified": "2021-08-19 14:09:11.643464",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Healthcare Service Unit", "name": "Healthcare Service Unit",
"nsm_parent_field": "parent_healthcare_service_unit",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@@ -5,14 +5,21 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
from frappe.utils import cint, cstr
import frappe import frappe
from frappe import _
import json
class HealthcareServiceUnit(NestedSet): class HealthcareServiceUnit(NestedSet):
nsm_parent_field = 'parent_healthcare_service_unit' nsm_parent_field = 'parent_healthcare_service_unit'
def validate(self):
self.set_service_unit_properties()
def autoname(self): def autoname(self):
if self.company: 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): if not self.healthcare_service_unit_name.endswith(suffix):
self.name = self.healthcare_service_unit_name + suffix self.name = self.healthcare_service_unit_name + suffix
else: else:
@@ -22,16 +29,86 @@ class HealthcareServiceUnit(NestedSet):
super(HealthcareServiceUnit, self).on_update() super(HealthcareServiceUnit, self).on_update()
self.validate_one_root() self.validate_one_root()
def after_insert(self): def set_service_unit_properties(self):
if self.is_group: if self.is_group:
self.allow_appointments = 0 self.allow_appointments = False
self.overlap_appointments = 0 self.overlap_appointments = False
self.inpatient_occupancy = 0 self.inpatient_occupancy = False
elif self.service_unit_type: 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) service_unit_type = frappe.get_doc('Healthcare Service Unit Type', self.service_unit_type)
self.allow_appointments = service_unit_type.allow_appointments self.allow_appointments = service_unit_type.allow_appointments
self.overlap_appointments = service_unit_type.overlap_appointments
self.inpatient_occupancy = service_unit_type.inpatient_occupancy 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.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"] = { frappe.provide("frappe.treeview_settings");
breadcrumbs: "Healthcare Service Unit",
title: __("Healthcare Service Unit"), frappe.treeview_settings['Healthcare Service Unit'] = {
breadcrumbs: 'Healthcare Service Unit',
title: __('Service Unit Tree'),
get_tree_root: false, 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', get_tree_nodes: 'erpnext.healthcare.utils.get_children',
ignore_fields:["parent_healthcare_service_unit"], filters: [{
onrender: function(node) { fieldname: 'company',
if (node.data.occupied_out_of_vacant!==undefined) { fieldtype: 'Select',
$('<span class="balance-area pull-right">' options: erpnext.utils.get_tree_options('company'),
+ " " + node.data.occupied_out_of_vacant 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); + '</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.inpatient_occupancy == 1) {
if (node.data.occupancy_status == "Occupied") { if (node.data.occupancy_status == 'Occupied') {
$('<span class="balance-area pull-right">' $("<span class='balance-area pull-right small'>"
+ " " + node.data.occupancy_status + ' ' + node.data.occupancy_status
+ '</span>').insertBefore(node.$ul); + '</span>').insertBefore(node.$ul);
} }
if (node.data.occupancy_status == "Vacant") { if (node.data.occupancy_status == 'Vacant') {
$('<span class="balance-area pull-right">' $("<span class='balance-area pull-right text-muted small'>"
+ " " + node.data.occupancy_status + ' ' + node.data.occupancy_status
+ '</span>').insertBefore(node.$ul); + '</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) { if (values) {
frappe.call({ frappe.call({
"method": "erpnext.healthcare.doctype.healthcare_service_unit_type.healthcare_service_unit_type.change_item_code", "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}, "args": { item: doc.item, item_code: values['item_code'], doc_name: doc.name },
callback: function () { callback: function() {
frm.reload_doc(); frm.reload_doc();
} }
}); });

View File

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

View File

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

View File

@@ -151,7 +151,7 @@ def get_healthcare_service_unit(unit_name=None):
if not service_unit: if not service_unit:
service_unit = frappe.new_doc("Healthcare 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.company = "_Test Company"
service_unit.service_unit_type = get_service_unit_type() service_unit.service_unit_type = get_service_unit_type()
service_unit.inpatient_occupancy = 1 service_unit.inpatient_occupancy = 1
@@ -159,12 +159,12 @@ def get_healthcare_service_unit(unit_name=None):
service_unit.is_group = 0 service_unit.is_group = 0
service_unit_parent_name = frappe.db.exists({ service_unit_parent_name = frappe.db.exists({
"doctype": "Healthcare Service Unit", "doctype": "Healthcare Service Unit",
"healthcare_service_unit_name": "All Healthcare Service Units", "healthcare_service_unit_name": "_Test All Healthcare Service Units",
"is_group": 1 "is_group": 1
}) })
if not service_unit_parent_name: if not service_unit_parent_name:
parent_service_unit = frappe.new_doc("Healthcare Service Unit") 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.is_group = 1
parent_service_unit.save(ignore_permissions = True) parent_service_unit.save(ignore_permissions = True)
service_unit.parent_healthcare_service_unit = parent_service_unit.name service_unit.parent_healthcare_service_unit = parent_service_unit.name
@@ -180,7 +180,7 @@ def get_service_unit_type():
if not service_unit_type: if not service_unit_type:
service_unit_type = frappe.new_doc("Healthcare 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.inpatient_occupancy = 1
service_unit_type.save(ignore_permissions = True) service_unit_type.save(ignore_permissions = True)
return service_unit_type.name 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')) { 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() { frm.add_custom_button(__('Patient History'), function() {
frappe.route_options = {'patient': frm.doc.name}; frappe.route_options = {'patient': frm.doc.name};
frappe.set_route('patient_history'); frappe.set_route('patient_history');
},'View'); }, __('View'));
} }
if (!frm.doc.__islocal && (frappe.user.has_role('Nursing User') || frappe.user.has_role('Physician'))) { frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Patient'};
frm.add_custom_button(__('Vital Signs'), function () { frm.toggle_display(['address_html', 'contact_html'], !frm.is_new());
create_vital_signs(frm);
}, 'Create'); if (!frm.is_new()) {
frm.add_custom_button(__('Medical Record'), function () { if ((frappe.user.has_role('Nursing User') || frappe.user.has_role('Physician'))) {
create_medical_record(frm); frm.add_custom_button(__('Medical Record'), function () {
}, 'Create'); create_medical_record(frm);
frm.add_custom_button(__('Patient Encounter'), function () { }, 'Create');
create_encounter(frm); frm.toggle_enable(['customer'], 0);
}, 'Create'); }
frm.toggle_enable(['customer'], 0); // ToDo, allow change only if no transactions booked or better, add merge option 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) { onload: function (frm) {
if (!frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html('');
}
if (frm.doc.dob) { if (frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(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) { if (frm.doc.dob) {
let today = new Date(); let today = new Date();
let birthDate = new Date(frm.doc.dob); let birthDate = new Date(frm.doc.dob);
if (today < birthDate){ if (today < birthDate) {
frappe.msgprint(__('Please select a valid Date')); frappe.msgprint(__('Please select a valid Date'));
frappe.model.set_value(frm.doctype,frm.docname, 'dob', ''); frappe.model.set_value(frm.doctype,frm.docname, 'dob', '');
} } else {
else {
let age_str = get_age(frm.doc.dob); let age_str = get_age(frm.doc.dob);
$(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${age_str}`); $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${age_str}`);
} }
} } else {
else {
$(frm.fields_dict['age_html'].wrapper).html(''); $(frm.fields_dict['age_html'].wrapper).html('');
} }
}); });

View File

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

View File

@@ -8,24 +8,27 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, cstr, getdate from frappe.utils import cint, cstr, getdate
import dateutil 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.model.naming import set_name_by_naming_series
from frappe.utils.nestedset import get_root_of from frappe.utils.nestedset import get_root_of
from erpnext import get_default_currency 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.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): class Patient(Document):
def onload(self):
'''Load address and contacts in `__onload`'''
load_address_and_contact(self)
self.load_dashboard_info()
def validate(self): def validate(self):
self.set_full_name() self.set_full_name()
self.add_as_website_user()
def before_insert(self): def before_insert(self):
self.set_missing_customer_details() self.set_missing_customer_details()
def after_insert(self): 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'): if frappe.db.get_single_value('Healthcare Settings', 'collect_registration_fee'):
frappe.db.set_value('Patient', self.name, 'status', 'Disabled') frappe.db.set_value('Patient', self.name, 'status', 'Disabled')
else: else:
@@ -50,6 +53,16 @@ class Patient(Document):
else: else:
create_customer(self) 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): def set_full_name(self):
if self.last_name: if self.last_name:
self.patient_name = ' '.join(filter(None, [self.first_name, 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: if not self.language:
self.language = frappe.db.get_single_value('System Settings', 'language') self.language = frappe.db.get_single_value('System Settings', 'language')
def add_as_website_user(self): def create_website_user(self):
if self.email: if self.email and not frappe.db.exists('User', self.email):
if not frappe.db.exists ('User', self.email): user = frappe.get_doc({
user = frappe.get_doc({ 'doctype': 'User',
'doctype': 'User', 'first_name': self.first_name,
'first_name': self.first_name, 'last_name': self.last_name,
'last_name': self.last_name, 'email': self.email,
'email': self.email, 'user_type': 'Website User',
'user_type': 'Website User' 'gender': self.sex,
}) 'phone': self.phone,
user.flags.ignore_permissions = True 'mobile_no': self.mobile,
user.add_roles('Patient') '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): def autoname(self):
patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by') patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by')
@@ -108,7 +127,8 @@ class Patient(Document):
if self.dob: if self.dob:
dob = getdate(self.dob) dob = getdate(self.dob)
age = dateutil.relativedelta.relativedelta(getdate(), 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 return age_str
@frappe.whitelist() @frappe.whitelist()
@@ -125,6 +145,58 @@ class Patient(Document):
return {'invoice': sales_invoice.name} 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): def create_customer(doc):
customer = frappe.get_doc({ customer = frappe.get_doc({
'doctype': 'Customer', 'doctype': 'Customer',
@@ -150,8 +222,8 @@ def make_invoice(patient, company):
sales_invoice.debit_to = get_receivable_account(company) sales_invoice.debit_to = get_receivable_account(company)
item_line = sales_invoice.append('items') item_line = sales_invoice.append('items')
item_line.item_name = 'Registeration Fee' item_line.item_name = 'Registration Fee'
item_line.description = 'Registeration Fee' item_line.description = 'Registration Fee'
item_line.qty = 1 item_line.qty = 1
item_line.uom = uom item_line.uom = uom
item_line.conversion_factor = 1 item_line.conversion_factor = 1
@@ -175,8 +247,11 @@ def get_patient_detail(patient):
return details return details
def get_timeline_data(doctype, name): 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 SELECT
unix_timestamp(communication_date), count(*) unix_timestamp(communication_date), count(*)
FROM FROM
@@ -185,3 +260,11 @@ def get_timeline_data(doctype, name):
patient=%s patient=%s
and `communication_date` > date_sub(curdate(), interval 1 year) and `communication_date` > date_sub(curdate(), interval 1 year)
GROUP BY communication_date''', name)) 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': True,
'heatmap_message': _('This is based on transactions against this Patient. See timeline below for details'), 'heatmap_message': _('This is based on transactions against this Patient. See timeline below for details'),
'fieldname': 'patient', 'fieldname': 'patient',
'non_standard_fieldnames': {
'Payment Entry': 'party'
},
'transactions': [ 'transactions': [
{ {
'label': _('Appointments and Patient Encounters'), 'label': _('Appointments and Encounters'),
'items': ['Patient Appointment', 'Patient Encounter'] 'items': ['Patient Appointment', 'Vital Signs', 'Patient Encounter']
}, },
{ {
'label': _('Lab Tests and Vital Signs'), 'label': _('Lab Tests and Vital Signs'),
'items': ['Lab Test', 'Sample Collection', 'Vital Signs'] 'items': ['Lab Test', 'Sample Collection']
}, },
{ {
'label': _('Billing'), 'label': _('Rehab and Physiotherapy'),
'items': ['Sales Invoice'] 'items': ['Patient Assessment', 'Therapy Session', 'Therapy Plan']
}, },
{ {
'label': _('Orders'), 'label': _('Surgery'),
'items': ['Inpatient Medication Order'] '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) { refresh: function(frm) {
frm.set_query('patient', function () { frm.set_query('patient', function() {
return { return {
filters: {'status': 'Active'} filters: { 'status': 'Active' }
}; };
}); });
@@ -64,7 +64,7 @@ frappe.ui.form.on('Patient Appointment', {
} else { } else {
frappe.call({ frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd', 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) { callback: function(data) {
if (data.message == true) { if (data.message == true) {
if (frm.doc.mode_of_payment && frm.doc.paid_amount) { if (frm.doc.mode_of_payment && frm.doc.paid_amount) {
@@ -97,7 +97,7 @@ frappe.ui.form.on('Patient Appointment', {
if (frm.doc.patient) { if (frm.doc.patient) {
frm.add_custom_button(__('Patient History'), function() { 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'); frappe.set_route('patient_history');
}, __('View')); }, __('View'));
} }
@@ -111,14 +111,14 @@ frappe.ui.form.on('Patient Appointment', {
}); });
if (frm.doc.procedure_template) { if (frm.doc.procedure_template) {
frm.add_custom_button(__('Clinical Procedure'), function(){ frm.add_custom_button(__('Clinical Procedure'), function() {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: 'erpnext.healthcare.doctype.clinical_procedure.clinical_procedure.make_procedure', method: 'erpnext.healthcare.doctype.clinical_procedure.clinical_procedure.make_procedure',
frm: frm, frm: frm,
}); });
}, __('Create')); }, __('Create'));
} else if (frm.doc.therapy_type) { } 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({ frappe.model.open_mapped_doc({
method: 'erpnext.healthcare.doctype.therapy_session.therapy_session.create_therapy_session', method: 'erpnext.healthcare.doctype.therapy_session.therapy_session.create_therapy_session',
frm: frm, frm: frm,
@@ -148,7 +148,7 @@ frappe.ui.form.on('Patient Appointment', {
doctype: 'Patient', doctype: 'Patient',
name: frm.doc.patient name: frm.doc.patient
}, },
callback: function (data) { callback: function(data) {
let age = null; let age = null;
if (data.message.dob) { if (data.message.dob) {
age = calculate_age(data.message.dob); age = calculate_age(data.message.dob);
@@ -165,7 +165,7 @@ frappe.ui.form.on('Patient Appointment', {
}, },
practitioner: function(frm) { practitioner: function(frm) {
if (frm.doc.practitioner ) { if (frm.doc.practitioner) {
frm.events.set_payment_details(frm); frm.events.set_payment_details(frm);
} }
}, },
@@ -230,7 +230,7 @@ frappe.ui.form.on('Patient Appointment', {
toggle_payment_fields: function(frm) { toggle_payment_fields: function(frm) {
frappe.call({ frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd', 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) { callback: function(data) {
if (data.message.fee_validity) { if (data.message.fee_validity) {
// if fee validity exists and automated appointment invoicing is enabled, // 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('paid_amount', data.message ? 1 : 0);
frm.toggle_display('billing_item', 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('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); frm.toggle_reqd('billing_item', data.message ? 1 : 0);
} }
} }
@@ -258,7 +258,7 @@ frappe.ui.form.on('Patient Appointment', {
if (frm.doc.patient) { if (frm.doc.patient) {
frappe.call({ frappe.call({
method: "erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_prescribed_therapies", method: "erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_prescribed_therapies",
args: {patient: frm.doc.patient}, args: { patient: frm.doc.patient },
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {
show_therapy_types(frm, 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({ let d = new frappe.ui.Dialog({
title: __('Available slots'), title: __('Available slots'),
fields: [ fields: [
{ fieldtype: 'Link', options: 'Medical Department', reqd: 1, fieldname: 'department', label: 'Medical Department'}, { fieldtype: 'Link', options: 'Medical Department', reqd: 1, fieldname: 'department', label: 'Medical Department' },
{ fieldtype: 'Column Break'}, { fieldtype: 'Column Break' },
{ fieldtype: 'Link', options: 'Healthcare Practitioner', reqd: 1, fieldname: 'practitioner', label: 'Healthcare Practitioner'}, { fieldtype: 'Link', options: 'Healthcare Practitioner', reqd: 1, fieldname: 'practitioner', label: 'Healthcare Practitioner' },
{ fieldtype: 'Column Break'}, { fieldtype: 'Column Break' },
{ fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date'}, { fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date' },
{ fieldtype: 'Section Break'}, { fieldtype: 'Section Break' },
{ fieldtype: 'HTML', fieldname: 'available_slots'} { fieldtype: 'HTML', fieldname: 'available_slots' }
], ],
primary_action_label: __('Book'), primary_action_label: __('Book'),
@@ -379,59 +379,22 @@ let check_and_set_availability = function(frm) {
let $wrapper = d.fields_dict.available_slots.$wrapper; let $wrapper = d.fields_dict.available_slots.$wrapper;
// make buttons for each slot // make buttons for each slot
let slot_details = data.slot_details; let slot_html = get_slots(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/>`;
}
$wrapper $wrapper
.css('margin-bottom', 0) .css('margin-bottom', 0)
.addClass('text-center') .addClass('text-center')
.html(slot_html); .html(slot_html);
// blue button when clicked // highlight button when clicked
$wrapper.on('click', 'button', function() { $wrapper.on('click', 'button', function() {
let $btn = $(this); let $btn = $(this);
$wrapper.find('button').removeClass('btn-primary'); $wrapper.find('button').removeClass('btn-outline-primary');
$btn.addClass('btn-primary'); $btn.addClass('btn-outline-primary');
selected_slot = $btn.attr('data-name'); selected_slot = $btn.attr('data-name');
service_unit = $btn.attr('data-service-unit'); service_unit = $btn.attr('data-service-unit');
duration = $btn.attr('data-duration'); duration = $btn.attr('data-duration');
// enable dialog action // enable primary action 'Book'
d.get_primary_btn().attr('disabled', null); d.get_primary_btn().attr('disabled', null);
}); });
@@ -441,19 +404,102 @@ let check_and_set_availability = function(frm) {
} }
}, },
freeze: true, freeze: true,
freeze_message: __('Fetching records......') freeze_message: __('Fetching Schedule...')
}); });
} else { } else {
fd.available_slots.html(__('Appointment date and Healthcare Practitioner are Mandatory').bold()); 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) { let get_prescribed_procedure = function(frm) {
if (frm.doc.patient) { if (frm.doc.patient) {
frappe.call({ frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_procedure_prescribed', method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_procedure_prescribed',
args: {patient: frm.doc.patient}, args: { patient: frm.doc.patient },
callback: function(r) { callback: function(r) {
if (r.message && r.message.length) { if (r.message && r.message.length) {
show_procedure_templates(frm, r.message); 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({ let d = new frappe.ui.Dialog({
title: __('Prescribed Procedures'), title: __('Prescribed Procedures'),
fields: [ fields: [
@@ -493,9 +539,11 @@ let show_procedure_templates = function(frm, result){
data-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\ data-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\
data-date="%(date)s" data-department="%(department)s">\ data-date="%(date)s" data-department="%(department)s">\
<button class="btn btn-default btn-xs">Add\ <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], </button></a></div></div><div class="col-xs-12"><hr/><div/>', {
encounter:y[2], consulting_practitioner:y[3], encounter_date:y[4], name: y[0], procedure_template: y[1],
practitioner:y[5]? y[5]:'', date: y[6]? y[6]:'', department: y[7]? y[7]:''})).appendTo(html_field); 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() { row.find("a").click(function() {
frm.doc.procedure_template = $(this).attr('data-procedure-template'); frm.doc.procedure_template = $(this).attr('data-procedure-template');
frm.doc.procedure_prescription = $(this).attr('data-name'); frm.doc.procedure_prescription = $(this).attr('data-name');
@@ -513,7 +561,7 @@ let show_procedure_templates = function(frm, result){
}); });
if (!result) { if (!result) {
let msg = __('There are no procedure prescribed for ') + frm.doc.patient; 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(); d.show();
}; };
@@ -528,7 +576,7 @@ let show_therapy_types = function(frm, result) {
] ]
}); });
var html_field = d.fields_dict.therapy_type.$wrapper; 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;" >\ 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"> %(encounter)s <br> %(practitioner)s <br> %(date)s </div>\
<div class="col-xs-5"> %(therapy)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-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\
data-date="%(date)s" data-department="%(department)s">\ data-date="%(date)s" data-department="%(department)s">\
<button class="btn btn-default btn-xs">Add\ <button class="btn btn-default btn-xs">Add\
</button></a></div></div><div class="col-xs-12"><hr/><div/>', {therapy:y[0], </button></a></div></div><div class="col-xs-12"><hr/><div/>', {
name: y[1], encounter:y[2], practitioner:y[3], date:y[4], therapy: y[0],
department:y[6]? y[6]:'', therapy_plan:y[5]})).appendTo(html_field); 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() { row.find("a").click(function() {
frm.doc.therapy_type = $(this).attr("data-therapy"); frm.doc.therapy_type = $(this).attr("data-therapy");
@@ -574,13 +624,13 @@ let create_vital_signs = function(frm) {
frappe.new_doc('Vital Signs'); frappe.new_doc('Vital Signs');
}; };
let update_status = function(frm, status){ let update_status = function(frm, status) {
let doc = frm.doc; let doc = frm.doc;
frappe.confirm(__('Are you sure you want to cancel this appointment?'), frappe.confirm(__('Are you sure you want to cancel this appointment?'),
function() { function() {
frappe.call({ frappe.call({
method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_status', 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) { callback: function(data) {
if (!data.exc) { if (!data.exc) {
frm.reload_doc(); frm.reload_doc();

View File

@@ -131,7 +131,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Service Unit", "label": "Service Unit",
"options": "Healthcare Service Unit", "options": "Healthcare Service Unit",
"set_only_once": 1 "read_only": 1
}, },
{ {
"fieldname": "section_break_12", "fieldname": "section_break_12",
@@ -349,7 +349,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-02-08 13:13:15.116833", "modified": "2021-08-19 17:28:41.329387",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Patient Appointment", "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.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 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): class PatientAppointment(Document):
def validate(self): def validate(self):
self.validate_overlaps() self.validate_overlaps()
@@ -49,26 +54,49 @@ class PatientAppointment(Document):
end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \ end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \
+ datetime.timedelta(minutes=flt(self.duration)) + datetime.timedelta(minutes=flt(self.duration))
overlaps = frappe.db.sql(""" # all appointments for both patient and practitioner overlapping the duration of this appointment
select overlapping_appointments = frappe.db.sql("""
name, practitioner, patient, appointment_time, duration SELECT
from name, practitioner, patient, appointment_time, duration, service_unit
`tabPatient Appointment` FROM
where `tabPatient Appointment`
appointment_date=%s and name!=%s and status NOT IN ("Closed", "Cancelled") WHERE
and (practitioner=%s or patient=%s) and appointment_date=%(appointment_date)s AND name!=%(name)s AND status NOT IN ("Closed", "Cancelled") AND
((appointment_time<%s and appointment_time + INTERVAL duration MINUTE>%s) or (practitioner=%(practitioner)s OR patient=%(patient)s) AND
(appointment_time>%s and appointment_time<%s) or ((appointment_time<%(appointment_time)s AND appointment_time + INTERVAL duration MINUTE>%(appointment_time)s) OR
(appointment_time=%s)) (appointment_time>%(appointment_time)s AND appointment_time<%(end_time)s) OR
""", (self.appointment_date, self.name, self.practitioner, self.patient, (appointment_time=%(appointment_time)s))
self.appointment_time, end_time.time(), self.appointment_time, end_time.time(), self.appointment_time)) """,
{
'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): def validate_service_unit(self):
if self.inpatient_record and self.service_unit: if self.inpatient_record and self.service_unit:
@@ -327,6 +355,8 @@ def get_available_slots(practitioner_doc, date):
if available_slots: if available_slots:
appointments = [] appointments = []
allow_overlap = 0
service_unit_capacity = 0
# fetch all appointments to practitioner by service unit # fetch all appointments to practitioner by service unit
filters = { filters = {
'practitioner': practitioner, 'practitioner': practitioner,
@@ -336,8 +366,8 @@ def get_available_slots(practitioner_doc, date):
} }
if schedule_entry.service_unit: if schedule_entry.service_unit:
slot_name = schedule_entry.schedule + ' - ' + schedule_entry.service_unit slot_name = f'{schedule_entry.schedule}'
allow_overlap = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, 'overlap_appointments') allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, ['overlap_appointments', 'service_unit_capacity'])
if not allow_overlap: if not allow_overlap:
# fetch all appointments to service unit # fetch all appointments to service unit
filters.pop('practitioner') filters.pop('practitioner')
@@ -352,8 +382,8 @@ def get_available_slots(practitioner_doc, date):
filters=filters, filters=filters,
fields=['name', 'appointment_time', 'duration', 'status']) fields=['name', 'appointment_time', 'duration', 'status'])
slot_details.append({'slot_name':slot_name, 'service_unit':schedule_entry.service_unit, slot_details.append({'slot_name': slot_name, 'service_unit': schedule_entry.service_unit, 'avail_slot': available_slots,
'avail_slot':available_slots, 'appointments': appointments}) 'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity})
return slot_details 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 `tabFee Validity`""")
frappe.db.sql("""delete from `tabPatient Encounter`""") frappe.db.sql("""delete from `tabPatient Encounter`""")
make_pos_profile() 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): 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) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
appointment = create_appointment(patient, practitioner, nowdate()) appointment = create_appointment(patient, practitioner, nowdate())
self.assertEqual(appointment.status, 'Open') 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') self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
def test_start_encounter(self): 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) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1)
appointment.reload() appointment.reload()
@@ -43,7 +45,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced')) self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'))
def test_auto_invoicing(self): 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, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
appointment = create_appointment(patient, practitioner, nowdate()) 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) self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
def test_auto_invoicing_based_on_department(self): 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, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment_type = create_appointment_type() 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) 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): 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, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
@@ -103,7 +105,7 @@ class TestPatientAppointment(unittest.TestCase):
self.assertTrue(sales_invoice_name) self.assertTrue(sales_invoice_name)
def test_appointment_cancel(self): 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) frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
appointment = create_appointment(patient, practitioner, nowdate()) appointment = create_appointment(patient, practitioner, nowdate())
fee_validity = frappe.db.get_value('Fee Validity Reference', {'appointment': appointment.name}, 'parent') 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 create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""") frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs() patient, practitioner = create_healthcare_docs()
patient = create_patient() patient = create_patient()
# Schedule Admission # Schedule Admission
ip_record = create_inpatient(patient) ip_record = create_inpatient(patient)
@@ -137,7 +139,7 @@ class TestPatientAppointment(unittest.TestCase):
ip_record.save(ignore_permissions = True) ip_record.save(ignore_permissions = True)
# Admit # 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()) admit_patient(ip_record, service_unit, now_datetime())
appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit) 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 create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
frappe.db.sql("""delete from `tabInpatient Record`""") frappe.db.sql("""delete from `tabInpatient Record`""")
patient, medical_department, practitioner = create_healthcare_docs() patient, practitioner = create_healthcare_docs()
patient = create_patient() patient = create_patient()
# Schedule Admission # Schedule Admission
ip_record = create_inpatient(patient) ip_record = create_inpatient(patient)
@@ -163,10 +165,10 @@ class TestPatientAppointment(unittest.TestCase):
ip_record.save(ignore_permissions = True) ip_record.save(ignore_permissions = True)
# Admit # 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()) 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) appointment = create_appointment(patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0)
self.assertRaises(frappe.exceptions.ValidationError, appointment.save) self.assertRaises(frappe.exceptions.ValidationError, appointment.save)
@@ -176,39 +178,102 @@ class TestPatientAppointment(unittest.TestCase):
mark_invoiced_inpatient_occupancy(ip_record1) mark_invoiced_inpatient_occupancy(ip_record1)
discharge_patient(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 and practitioner cannot have overlapping appointments
patient = create_patient() appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit, save=0)
practitioner = frappe.db.exists('Healthcare Practitioner', '_Test Healthcare Practitioner') self.assertRaises(OverlapError, appointment.save)
medical_department = frappe.db.exists('Medical Department', '_Test Medical Department') 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: # patient cannot have overlapping appointments with other practitioners
medical_department = frappe.new_doc('Medical Department') appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit, save=0)
medical_department.department = '_Test Medical Department' self.assertRaises(OverlapError, appointment.save)
medical_department.save(ignore_permissions=True) appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit_1, save=0)
medical_department = medical_department.name self.assertRaises(OverlapError, appointment.save)
appointment = create_appointment(patient, practitioner_1, nowdate(), save=0)
self.assertRaises(OverlapError, appointment.save)
if not practitioner: # practitioner cannot have overlapping appointments with other patients
practitioner = frappe.new_doc('Healthcare Practitioner') appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit, save=0)
practitioner.first_name = '_Test Healthcare Practitioner' self.assertRaises(OverlapError, appointment.save)
practitioner.gender = 'Female' appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit_1, save=0)
practitioner.department = medical_department self.assertRaises(OverlapError, appointment.save)
practitioner.op_consulting_charge = 500 appointment = create_appointment(patient_1, practitioner, nowdate(), save=0)
practitioner.inpatient_visit_charge = 500 self.assertRaises(OverlapError, appointment.save)
practitioner.save(ignore_permissions=True)
practitioner = practitioner.name
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): def create_encounter(appointment):
if appointment: if appointment:
@@ -221,8 +286,10 @@ def create_encounter(appointment):
encounter.company = appointment.company encounter.company = appointment.company
encounter.save() encounter.save()
encounter.submit() encounter.submit()
return encounter return encounter
def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0,
service_unit=None, appointment_type=None, save=1, department=None): service_unit=None, appointment_type=None, save=1, department=None):
item = create_healthcare_service_items() 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.appointment_date = appointment_date
appointment.company = '_Test Company' appointment.company = '_Test Company'
appointment.duration = 15 appointment.duration = 15
if service_unit: if service_unit:
appointment.service_unit = service_unit appointment.service_unit = service_unit
if invoice: 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') appointment.procedure_template = create_clinical_procedure_template().get('name')
if save: if save:
appointment.save(ignore_permissions=True) appointment.save(ignore_permissions=True)
return appointment return appointment
def create_healthcare_service_items(): def create_healthcare_service_items():
if frappe.db.exists('Item', 'HLC-SI-001'): if frappe.db.exists('Item', 'HLC-SI-001'):
return 'HLC-SI-001' return 'HLC-SI-001'
item = frappe.new_doc('Item') item = frappe.new_doc('Item')
item.item_code = 'HLC-SI-001' item.item_code = 'HLC-SI-001'
item.item_name = 'Consulting Charges' item.item_name = 'Consulting Charges'
@@ -257,11 +328,14 @@ def create_healthcare_service_items():
item.is_stock_item = 0 item.is_stock_item = 0
item.stock_uom = 'Nos' item.stock_uom = 'Nos'
item.save() item.save()
return item.name return item.name
def create_clinical_procedure_template(): def create_clinical_procedure_template():
if frappe.db.exists('Clinical Procedure Template', 'Knee Surgery and Rehab'): if frappe.db.exists('Clinical Procedure Template', 'Knee Surgery and Rehab'):
return frappe.get_doc('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 = frappe.new_doc('Clinical Procedure Template')
template.template = 'Knee Surgery and Rehab' template.template = 'Knee Surgery and Rehab'
template.item_code = '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.description = 'Knee Surgery and Rehab'
template.rate = 50000 template.rate = 50000
template.save() template.save()
return template return template
def create_appointment_type(args=None): def create_appointment_type(args=None):
if not args: if not args:
args = frappe.local.form_dict 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}), 'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}),
'items': args.get('items') or items 'items': args.get('items') or items
}).insert() }).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) }, target_doc)
return doc return doc

View File

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from frappe.utils import nowdate 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 from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
class TestPatientMedicalRecord(unittest.TestCase): class TestPatientMedicalRecord(unittest.TestCase):
@@ -15,7 +15,8 @@ class TestPatientMedicalRecord(unittest.TestCase):
make_pos_profile() make_pos_profile()
def test_medical_record(self): 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) appointment = create_appointment(patient, practitioner, nowdate(), invoice=1)
encounter = create_encounter(appointment) encounter = create_encounter(appointment)

View File

@@ -8,11 +8,13 @@ import unittest
from frappe.utils import getdate, flt, nowdate 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_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.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): class TestTherapyPlan(unittest.TestCase):
def test_creation_on_encounter_submission(self): 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) encounter = create_encounter(patient, medical_department, practitioner)
self.assertTrue(frappe.db.exists('Therapy Plan', encounter.therapy_plan)) self.assertTrue(frappe.db.exists('Therapy Plan', encounter.therapy_plan))
@@ -28,8 +30,9 @@ class TestTherapyPlan(unittest.TestCase):
frappe.get_doc(session).submit() frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed') 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()) appointment = create_appointment(patient, practitioner, nowdate())
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name) session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
session = frappe.get_doc(session) session = frappe.get_doc(session)
session.submit() session.submit()

View File

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

View File

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

View File

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

View File

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

View File

@@ -543,58 +543,43 @@ def get_drugs_to_invoice(encounter):
@frappe.whitelist() @frappe.whitelist()
def get_children(doctype, parent, company, is_root=False): def get_children(doctype, parent=None, company=None, is_root=False):
parent_fieldname = "parent_" + doctype.lower().replace(" ", "_") parent_fieldname = 'parent_' + doctype.lower().replace(' ', '_')
fields = [ fields = [
"name as value", 'name as value',
"is_group as expandable", 'is_group as expandable',
"lft", 'lft',
"rgt" '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: if is_root:
fields += ["service_unit_type"] if doctype == "Healthcare Service Unit" else [] fields += ['service_unit_type'] if doctype == 'Healthcare Service Unit' else []
filters.append(["company", "=", company]) filters.append(['company', '=', company])
else: else:
fields += ["service_unit_type", "allow_appointments", "inpatient_occupancy", "occupancy_status"] if doctype == "Healthcare Service Unit" else [] fields += ['service_unit_type', 'allow_appointments', 'inpatient_occupancy',
fields += [parent_fieldname + " as parent"] '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": if available_count > 0:
for each in hc_service_units: occupied_count = frappe.db.count('Healthcare Service Unit', {
occupancy_msg = "" 'parent_healthcare_service_unit': each['value'],
if each["expandable"] == 1: 'inpatient_occupancy': 1,
occupied = False 'occupancy_status': 'Occupied'})
vacant = False # set occupancy status of group node
child_list = frappe.db.sql( each['occupied_of_available'] = str(
''' occupied_count) + ' Occupied of ' + str(available_count)
SELECT
name, occupancy_status
FROM
`tabHealthcare Service Unit`
WHERE
inpatient_occupancy = 1
and lft > %s and rgt < %s
''', (each['lft'], each['rgt']))
for child in child_list: return service_units
if not occupied:
occupied = 0
if child[1] == "Occupied":
occupied += 1
if not vacant:
vacant = 0
if child[1] == "Vacant":
vacant += 1
if vacant and occupied:
occupancy_total = vacant + occupied
occupancy_msg = str(occupied) + " Occupied out of " + str(occupancy_total)
each["occupied_out_of_vacant"] = occupancy_msg
return hc_service_units
@frappe.whitelist() @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>' 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} 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" "on_trash": "erpnext.regional.check_deletion_permission"
}, },
'Address': { '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': { 'Supplier': {
'validate': 'erpnext.regional.india.utils.validate_pan_for_india' 'validate': 'erpnext.regional.india.utils.validate_pan_for_india'
@@ -293,13 +298,16 @@ doc_events = {
"Contact": { "Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue", "on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations", "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": { "Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
}, },
('Quotation', 'Sales Order', 'Sales Invoice'): { ('Quotation', 'Sales Order', 'Sales Invoice'): {
'validate': ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"] '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() () => done()
]); ]);
}); });

View File

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

View File

@@ -267,5 +267,3 @@ erpnext.EmployeeSelector = Class.extend({
mark_employee_toolbar.appendTo($(this.wrapper)); 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): def find_index_in_dict(dict_list, key, value):
return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None) 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("Invalid"),
bold("Resolved")) bold("Resolved"))
) )

View File

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

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