From 6cbe331033cc1bc079c24c0a03ae13556bc798df Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Tue, 13 Jun 2023 20:31:31 +0530 Subject: [PATCH 01/39] fix(ci): failing server tests (cherry picked from commit dc05b163d9c96e615a7152775377b1de35d40bc4) --- .github/helper/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index b54cdebb77..5dd9cc840f 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -4,7 +4,7 @@ set -ex cd ~ || exit -sudo apt-get install redis-server libcups2-dev -qq +sudo apt-get update && sudo apt-get install redis-server libcups2-dev --fix-missing pip install frappe-bench From dde8413adeba3a80644ff0cd914514c7e628c917 Mon Sep 17 00:00:00 2001 From: Akash Date: Mon, 3 Apr 2023 18:21:55 +0530 Subject: [PATCH 02/39] fix: remove hardwired fiscal year in before_tests method (cherry picked from commit fe2c33745fe624227715c00e2709556db5e8928a) --- healthcare/healthcare/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index a9ac66fd68..66117cc9c6 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -904,6 +904,8 @@ def before_tests(): # complete setup if missing from frappe.desk.page.setup_wizard.setup_wizard import setup_complete + current_year = frappe.utils.now_datetime().year + if not frappe.get_list("Company"): setup_complete( { @@ -914,8 +916,8 @@ def before_tests(): "company_abbr": "WP", "industry": "Healthcare", "country": "United States", - "fy_start_date": "2022-04-01", - "fy_end_date": "2023-03-31", + "fy_start_date": f"{current_year}-01-01", + "fy_end_date": f"{current_year}-12-31", "language": "english", "company_tagline": "Testing", "email": "test@erpnext.com", From 835b6354f19a13221a52bf54194ab97a4dc62845 Mon Sep 17 00:00:00 2001 From: Akash Date: Wed, 29 Mar 2023 16:32:59 +0530 Subject: [PATCH 03/39] fix: Get Applicable Treatment Plan for new Patient Encounter (cherry picked from commit 421f136ae0ccd5429c6fa5c6cb43bd97b05b127e) --- .../doctype/patient_encounter/patient_encounter.js | 6 ++---- .../doctype/patient_encounter/patient_encounter.py | 7 ++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/healthcare/healthcare/doctype/patient_encounter/patient_encounter.js b/healthcare/healthcare/doctype/patient_encounter/patient_encounter.js index d18c03b7f9..4ce3b4109c 100644 --- a/healthcare/healthcare/doctype/patient_encounter/patient_encounter.js +++ b/healthcare/healthcare/doctype/patient_encounter/patient_encounter.js @@ -249,10 +249,8 @@ frappe.ui.form.on('Patient Encounter', { doc: frm.doc, args: selections, }).then(() => { - frm.refresh_field('drug_prescription'); - frm.refresh_field('procedure_prescription'); - frm.refresh_field('lab_test_prescription'); - frm.refresh_field('therapies'); + frm.refresh_fields(); + frm.dirty(); }); cur_dialog.hide(); } diff --git a/healthcare/healthcare/doctype/patient_encounter/patient_encounter.py b/healthcare/healthcare/doctype/patient_encounter/patient_encounter.py index 022023b827..b6d0fccc15 100644 --- a/healthcare/healthcare/doctype/patient_encounter/patient_encounter.py +++ b/healthcare/healthcare/doctype/patient_encounter/patient_encounter.py @@ -99,8 +99,6 @@ def set_treatment_plan(self, plan): for drug in drugs: self.append("drug_prescription", drug) - self.save() - def set_treatment_plan_item(self, plan_item): if plan_item.type == "Clinical Procedure Template": self.append("procedure_prescription", {"procedure": plan_item.template}) @@ -109,7 +107,10 @@ def set_treatment_plan_item(self, plan_item): self.append("lab_test_prescription", {"lab_test_code": plan_item.template}) if plan_item.type == "Therapy Type": - self.append("therapies", {"therapy_type": plan_item.template}) + self.append( + "therapies", + {"therapy_type": plan_item.template, "no_of_sessions": plan_item.qty}, + ) @frappe.whitelist() From 0db5401abb1faecf54941ecd222a5036ed5c147d Mon Sep 17 00:00:00 2001 From: Akash Date: Mon, 24 Apr 2023 17:28:32 +0530 Subject: [PATCH 04/39] fix: frappe.db.exists check (cherry picked from commit 9fa351833671119b9adf34c8c8fb9f673a84f0a8) --- healthcare/healthcare/doctype/lab_test/lab_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/healthcare/healthcare/doctype/lab_test/lab_test.py b/healthcare/healthcare/doctype/lab_test/lab_test.py index 90ded101f4..17f8e4dc47 100644 --- a/healthcare/healthcare/doctype/lab_test/lab_test.py +++ b/healthcare/healthcare/doctype/lab_test/lab_test.py @@ -273,7 +273,7 @@ def create_sample_doc(template, patient, invoice, company=None): if sample_exists: # update sample collection by adding quantity - sample_collection = frappe.get_doc("Sample Collection", sample_exists[0][0]) + sample_collection = frappe.get_doc("Sample Collection", sample_exists) quantity = int(sample_collection.sample_qty) + int(template.sample_qty) if template.sample_details: sample_details = sample_collection.sample_details + "\n-\n" + _("Test :") From 02f52e76ca9828d19886eb1d4247417d76356a00 Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Mon, 24 Apr 2023 17:19:31 +0530 Subject: [PATCH 05/39] fix: Patient Appointment - reload doc after appointment invoiced (cherry picked from commit e4c7cd6c048fc755b161b882fcb6713713776299) --- .../doctype/patient_appointment/patient_appointment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py index ff66e3e5dc..2c8046552b 100755 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py @@ -353,6 +353,7 @@ def create_sales_invoice(appointment_doc): appointment_doc.name, {"invoiced": 1, "ref_sales_invoice": sales_invoice.name}, ) + appointment_doc.reload() def check_is_new_patient(patient, name=None): From 09e0367b66b588c40accaaa7292010fee9ad2adc Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Thu, 20 Apr 2023 15:55:39 +0530 Subject: [PATCH 06/39] fix: Patient Appointment - fee validity message formating (cherry picked from commit b60dc5b5e3e64e84aa0f97d75ca52e5097cfb066) --- .../doctype/patient_appointment/patient_appointment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py index 2c8046552b..f4476ea58a 100755 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py @@ -12,7 +12,7 @@ from frappe.core.doctype.sms_settings.sms_settings import send_sms from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc -from frappe.utils import flt, get_link_to_form, get_time, getdate +from frappe.utils import flt, format_date, get_link_to_form, get_time, getdate from healthcare.healthcare.doctype.healthcare_settings.healthcare_settings import ( get_income_account, @@ -194,8 +194,8 @@ def update_fee_validity(self): fee_validity = manage_fee_validity(self) if fee_validity: frappe.msgprint( - _("{0}: {1} has fee validity till {2}").format( - self.patient, frappe.bold(self.patient_name), fee_validity.valid_till + _("{0} has fee validity till {1}").format( + frappe.bold(self.patient_name), format_date(fee_validity.valid_till) ) ) From c2423f7d234fd1c7f86db0815efcb8f0e8217c87 Mon Sep 17 00:00:00 2001 From: Akash Date: Thu, 13 Apr 2023 19:04:48 +0530 Subject: [PATCH 07/39] fix: check item price exists on update of Therapy Type (cherry picked from commit 77d0a5c0ed45c2665a2b829505766963dcdc0c5f) --- .../doctype/therapy_type/therapy_type.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/healthcare/healthcare/doctype/therapy_type/therapy_type.py b/healthcare/healthcare/doctype/therapy_type/therapy_type.py index d524dd29a3..b4de83ee6a 100644 --- a/healthcare/healthcare/doctype/therapy_type/therapy_type.py +++ b/healthcare/healthcare/doctype/therapy_type/therapy_type.py @@ -11,6 +11,10 @@ from frappe.model.rename_doc import rename_doc from frappe.utils import cint +from healthcare.healthcare.doctype.clinical_procedure_template.clinical_procedure_template import ( + make_item_price, +) + class TherapyType(Document): def validate(self): @@ -41,11 +45,14 @@ def update_item_and_item_price(self): item_doc.save(ignore_permissions=True) if self.rate: - item_price = frappe.get_doc("Item Price", {"item_code": self.item}) - item_price.item_name = self.item_name - item_price.price_list_rate = self.rate - item_price.ignore_mandatory = True - item_price.save() + if frappe.db.exists("Item Price", {"item_code": self.item}): + item_price = frappe.get_doc("Item Price", {"item_code": self.item}) + item_price.item_name = self.item_name + item_price.price_list_rate = self.rate + item_price.ignore_mandatory = True + item_price.save() + else: + make_item_price(self.item, self.rate) elif not self.is_billable and self.item: frappe.db.set_value("Item", self.item, "disabled", 1) @@ -114,18 +121,6 @@ def create_item_from_therapy(doc): doc.db_set("item", item.name) -def make_item_price(item, item_price): - price_list_name = frappe.db.get_value("Price List", {"selling": 1}) - frappe.get_doc( - { - "doctype": "Item Price", - "price_list": price_list_name, - "item_code": item, - "price_list_rate": item_price, - } - ).insert(ignore_permissions=True, ignore_mandatory=True) - - @frappe.whitelist() def change_item_code_from_therapy(item_code, doc): doc = frappe._dict(json.loads(doc)) From 72ac3eb056e501239f276d5b9d2be7607ad0dc0e Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Wed, 12 Apr 2023 13:22:02 +0530 Subject: [PATCH 08/39] fix: Treatment Plan Template - Filter for practitioner based on medical department (cherry picked from commit c2c1d2719b73b5174fdd6efb768feeb5f50c96ba) --- .../treatment_plan_template/treatment_plan_template.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/healthcare/healthcare/doctype/treatment_plan_template/treatment_plan_template.js b/healthcare/healthcare/doctype/treatment_plan_template/treatment_plan_template.js index 986c3cb6e4..055f1e35e7 100644 --- a/healthcare/healthcare/doctype/treatment_plan_template/treatment_plan_template.js +++ b/healthcare/healthcare/doctype/treatment_plan_template/treatment_plan_template.js @@ -10,5 +10,15 @@ frappe.ui.form.on('Treatment Plan Template', { } }; }); + + frm.set_query("practitioners", function () { + if (frm.doc.medical_department) { + return { + filters: { + "department": frm.doc.medical_department + } + }; + } + }); }, }); From a5575ce3ca53627934ab2a4141bff17e56a95cb7 Mon Sep 17 00:00:00 2001 From: Akash Date: Wed, 15 Mar 2023 17:32:31 +0530 Subject: [PATCH 09/39] fix: set exercises in Therapy Session from Therapy Type on validate (cherry picked from commit c509042ae6c7328e5d1ead8c7e87e975e45443cc) --- .../healthcare/doctype/therapy_plan/therapy_plan.py | 7 ++++++- .../doctype/therapy_session/therapy_session.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/healthcare/healthcare/doctype/therapy_plan/therapy_plan.py b/healthcare/healthcare/doctype/therapy_plan/therapy_plan.py index 77ed9656ec..96767b03c1 100644 --- a/healthcare/healthcare/doctype/therapy_plan/therapy_plan.py +++ b/healthcare/healthcare/doctype/therapy_plan/therapy_plan.py @@ -64,7 +64,12 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company, appointme therapy_session.therapy_type = therapy_type.name therapy_session.duration = therapy_type.default_duration therapy_session.rate = therapy_type.rate - therapy_session.exercises = therapy_type.exercises + if not therapy_session.exercises and therapy_type.exercises: + for exercise in therapy_type.exercises: + therapy_session.append( + "exercises", + (frappe.copy_doc(exercise)).as_dict(), + ) therapy_session.appointment = appointment if frappe.flags.in_test: diff --git a/healthcare/healthcare/doctype/therapy_session/therapy_session.py b/healthcare/healthcare/doctype/therapy_session/therapy_session.py index 2c5c90aace..821070ccd1 100644 --- a/healthcare/healthcare/doctype/therapy_session/therapy_session.py +++ b/healthcare/healthcare/doctype/therapy_session/therapy_session.py @@ -21,6 +21,7 @@ class TherapySession(Document): def validate(self): + self.set_exercises_from_therapy_type() self.validate_duplicate() self.set_total_counts() @@ -110,6 +111,16 @@ def set_total_counts(self): self.db_set("total_counts_targeted", target_total) self.db_set("total_counts_completed", counts_completed) + def set_exercises_from_therapy_type(self): + if self.therapy_type and not self.exercises: + therapy_type_doc = frappe.get_cached_doc("Therapy Type", self.therapy_type) + if therapy_type_doc.exercises: + for exercise in therapy_type_doc.exercises: + self.append( + "exercises", + (frappe.copy_doc(exercise)).as_dict(), + ) + @frappe.whitelist() def create_therapy_session(source_name, target_doc=None): From 184cb050bf9e76ffe6392ab7cc8a12c4fa1c89e7 Mon Sep 17 00:00:00 2001 From: Akash Date: Wed, 15 Mar 2023 19:51:05 +0530 Subject: [PATCH 10/39] fix(test): test exercise set from Therapy Type (cherry picked from commit 0844dbb51837f180d9c8bf6e10b79cc9071a9d30) --- .../doctype/therapy_session/test_therapy_session.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/healthcare/healthcare/doctype/therapy_session/test_therapy_session.py b/healthcare/healthcare/doctype/therapy_session/test_therapy_session.py index 9f56595500..bbc16e81f4 100644 --- a/healthcare/healthcare/doctype/therapy_session/test_therapy_session.py +++ b/healthcare/healthcare/doctype/therapy_session/test_therapy_session.py @@ -7,9 +7,19 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate +from healthcare.healthcare.doctype.therapy_plan.test_therapy_plan import create_therapy_plan + class TestTherapySession(FrappeTestCase): - pass + def test_exercise_set_from_therapy_type(self): + plan = create_therapy_plan() + session = create_therapy_session(plan.patient, "Basic Rehab", plan.name) + if plan.therapy_plan_details: + therapy_type = frappe.get_doc("Therapy Type", plan.therapy_plan_details[0].therapy_type) + self.assertEqual( + session.exercises[0].exercise_type, + therapy_type.exercises[0].exercise_type, + ) def create_therapy_session(patient, therapy_type, therapy_plan, duration=0, start_date=None): From c9916193751a5bf405fdef71e1a35da7e4ad09e4 Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Thu, 13 Apr 2023 17:08:37 +0530 Subject: [PATCH 11/39] fix: Patient Appointment - Scheduler event update (cherry picked from commit a928536e80d96f182d8a8af0e56c5f925285dd8a) --- .../doctype/patient_appointment/patient_appointment.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py index f4476ea58a..fc048e2dde 100755 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py @@ -751,8 +751,10 @@ def get_prescribed_therapies(patient): def update_appointment_status(): # update the status of appointments daily appointments = frappe.get_all( - "Patient Appointment", {"status": ("not in", ["Closed", "Cancelled"])}, as_dict=1 + "Patient Appointment", {"status": ("not in", ["Closed", "Cancelled"])} ) for appointment in appointments: - frappe.get_doc("Patient Appointment", appointment.name).set_status() + appointment_doc = frappe.get_doc("Patient Appointment", appointment.name) + appointment_doc.set_status() + appointment_doc.save() From fe6bde2cbf223590c25ffcf75b64e53a80b6c613 Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Fri, 17 Mar 2023 14:05:07 +0530 Subject: [PATCH 12/39] fix: broken healthcare service unit tree view (cherry picked from commit aa88d9aa4df77891d97546795a3304a9df709409) --- .../healthcare_service_unit.json | 3 +- .../healthcare_service_unit_tree.js | 6 ++-- healthcare/healthcare/utils.py | 32 ++++++++++--------- healthcare/hooks.py | 4 +++ 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/healthcare/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json b/healthcare/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json index 0d0de47233..5d49a5a1ec 100644 --- a/healthcare/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json +++ b/healthcare/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json @@ -3,7 +3,6 @@ "allow_import": 1, "allow_rename": 1, "creation": "2016-09-21 13:48:14.731437", - "default_view": "Tree", "description": "Healthcare Service Unit", "doctype": "DocType", "document_type": "Setup", @@ -243,7 +242,7 @@ ], "is_tree": 1, "links": [], - "modified": "2023-03-03 19:01:12.459553", + "modified": "2023-03-17 12:01:33.459553", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Service Unit", diff --git a/healthcare/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js b/healthcare/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js index 570fce261b..24b8125f8b 100644 --- a/healthcare/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js +++ b/healthcare/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js @@ -66,19 +66,19 @@ frappe.treeview_settings['Healthcare Service Unit'] = { ignore_fields: ['parent_healthcare_service_unit'], onrender: function (node) { if (node.data.occupied_of_available !== undefined) { - $("" + $("" + ' ' + node.data.occupied_of_available + '').insertBefore(node.$ul); } if (node.data && node.data.inpatient_occupancy !== undefined) { if (node.data.inpatient_occupancy == 1) { if (node.data.occupancy_status == 'Occupied') { - $("" + $("" + ' ' + node.data.occupancy_status + '').insertBefore(node.$ul); } if (node.data.occupancy_status == 'Vacant') { - $("" + $("" + ' ' + node.data.occupancy_status + '').insertBefore(node.$ul); } diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index 66117cc9c6..e1f36d4318 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -651,23 +651,25 @@ def get_children(doctype, parent=None, company=None, is_root=False): 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( + if each["expandable"] != 1 or each["value"].startswith("All Healthcare Service Units"): + continue + + available_count = frappe.db.count( + "Healthcare Service Unit", + filters={"parent_healthcare_service_unit": each["value"], "inpatient_occupancy": 1}, + ) + + if available_count > 0: + occupied_count = frappe.db.count( "Healthcare Service Unit", - filters={"parent_healthcare_service_unit": each["value"], "inpatient_occupancy": 1}, + filters={ + "parent_healthcare_service_unit": each["value"], + "inpatient_occupancy": 1, + "occupancy_status": "Occupied", + }, ) - - if available_count > 0: - occupied_count = frappe.db.count( - "Healthcare Service Unit", - { - "parent_healthcare_service_unit": each["value"], - "inpatient_occupancy": 1, - "occupancy_status": "Occupied", - }, - ) - # set occupancy status of group node - each["occupied_of_available"] = str(occupied_count) + " Occupied of " + str(available_count) + # set occupancy status of group node + each["occupied_of_available"] = f"{str(occupied_count)} Occupied of {str(available_count)}" return service_units diff --git a/healthcare/hooks.py b/healthcare/hooks.py index 82b1a172c5..0db002babe 100644 --- a/healthcare/hooks.py +++ b/healthcare/hooks.py @@ -283,3 +283,7 @@ standard_queries = { "Healthcare Practitioner": "healthcare.healthcare.doctype.healthcare_practitioner.healthcare_practitioner.get_practitioner_list" } + +treeviews = [ + "Healthcare Service Unit", +] From 80d4f209999b3210549a5f5a6575407e1d5033d3 Mon Sep 17 00:00:00 2001 From: Akash Date: Sat, 1 Jul 2023 11:27:28 +0530 Subject: [PATCH 13/39] fix: failling server test, update node version (cherry picked from commit 97d5a362062c37eedbf6d982854ca7538e653494) --- .github/workflows/ci.yml | 2 +- .github/workflows/semantic-commits.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31b2efaac8..9816679ef1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 check-latest: true - name: Add to Hosts diff --git a/.github/workflows/semantic-commits.yml b/.github/workflows/semantic-commits.yml index d04ac2eb93..2632c99933 100644 --- a/.github/workflows/semantic-commits.yml +++ b/.github/workflows/semantic-commits.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 check-latest: true - name: Check commit titles From 7bc1eae2c7d52c3b80aa01b01e19f4cf7285faed Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Wed, 14 Jun 2023 18:43:44 +0530 Subject: [PATCH 14/39] refactor: db.get_single_value and db.set_single_value (cherry picked from commit ea1e4148518160e25b4f1a011ed02bca9ab31da4) --- .../healthcare_settings.py | 4 +- .../test_patient_appointment.py | 38 +++++++++---------- .../test_patient_medical_record.py | 4 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/healthcare/healthcare/doctype/healthcare_settings/healthcare_settings.py b/healthcare/healthcare/doctype/healthcare_settings/healthcare_settings.py index 5922d9c29c..bd36df8647 100644 --- a/healthcare/healthcare/doctype/healthcare_settings/healthcare_settings.py +++ b/healthcare/healthcare/doctype/healthcare_settings/healthcare_settings.py @@ -46,10 +46,10 @@ def get_sms_text(doc): doc = frappe.get_doc("Lab Test", doc) context = {"doc": doc, "alert": doc, "comments": None} - emailed = frappe.db.get_value("Healthcare Settings", None, "sms_emailed") + emailed = frappe.db.get_single_value("Healthcare Settings", "sms_emailed") sms_text["emailed"] = frappe.render_template(emailed, context) - printed = frappe.db.get_value("Healthcare Settings", None, "sms_printed") + printed = frappe.db.get_single_value("Healthcare Settings", "sms_printed") sms_text["printed"] = frappe.render_template(printed, context) return sms_text diff --git a/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py index fd5fa539f0..a8d8db0ea9 100644 --- a/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -31,7 +31,7 @@ def setUp(self): def test_status(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 0) + frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 0) appointment = create_appointment(patient, practitioner, nowdate()) self.assertEqual(appointment.status, "Open") appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2)) @@ -45,7 +45,7 @@ def test_status(self): def test_start_encounter(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice=1) appointment.reload() self.assertEqual(appointment.invoiced, 1) @@ -61,12 +61,12 @@ def test_start_encounter(self): def test_auto_invoicing(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 0) - frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 0) + frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) + frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 0) appointment = create_appointment(patient, practitioner, nowdate()) self.assertEqual(frappe.db.get_value("Patient Appointment", appointment.name, "invoiced"), 0) - frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), invoice=1) self.assertEqual(frappe.db.get_value("Patient Appointment", appointment.name, "invoiced"), 1) sales_invoice_name = frappe.db.get_value( @@ -86,8 +86,8 @@ def test_auto_invoicing(self): def test_auto_invoicing_based_on_department(self): patient, practitioner = create_healthcare_docs() medical_department = create_medical_department() - 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_single_value("Healthcare Settings", "enable_free_follow_ups", 0) + frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) appointment_type = create_appointment_type({"medical_department": medical_department}) appointment = create_appointment( @@ -114,8 +114,8 @@ def test_auto_invoicing_based_on_department(self): def test_auto_invoicing_according_to_appointment_type_charge(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 0) - frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) + frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) item = create_healthcare_service_items() items = [{"op_consulting_charge_item": item, "op_consulting_charge": 300}] @@ -139,7 +139,7 @@ def test_auto_invoicing_according_to_appointment_type_charge(self): def test_appointment_cancel(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 1) + frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 1) appointment = create_appointment(patient, practitioner, nowdate()) fee_validity = frappe.db.get_value( "Fee Validity", {"patient": patient, "practitioner": practitioner} @@ -155,8 +155,8 @@ def test_appointment_cancel(self): # check fee validity updated self.assertEqual(frappe.db.get_value("Fee Validity", fee_validity, "visited"), 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_single_value("Healthcare Settings", "enable_free_follow_ups", 0) + frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1) update_status(appointment.name, "Cancelled") # check invoice cancelled @@ -237,10 +237,10 @@ def test_invalid_healthcare_service_unit_validation(self): discharge_patient(ip_record1) def test_payment_should_be_mandatory_for_new_patient_appointment(self): - frappe.db.set_value("Healthcare Settings", None, "enable_free_follow_ups", 1) - frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) - frappe.db.set_value("Healthcare Settings", None, "max_visits", 3) - frappe.db.set_value("Healthcare Settings", None, "valid_days", 30) + frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 1) + frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "max_visits", 3) + frappe.db.set_single_value("Healthcare Settings", "valid_days", 30) patient = create_patient() assert check_is_new_patient(patient) @@ -249,7 +249,7 @@ def test_payment_should_be_mandatory_for_new_patient_appointment(self): def test_sales_invoice_should_be_generated_for_new_patient_appointment(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_value("Healthcare Settings", None, "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) invoice_count = frappe.db.count("Sales Invoice") assert check_is_new_patient(patient) @@ -449,8 +449,8 @@ def create_appointment( department=None, ): item = create_healthcare_service_items() - frappe.db.set_value("Healthcare Settings", None, "inpatient_visit_charge_item", item) - frappe.db.set_value("Healthcare Settings", None, "op_consulting_charge_item", item) + frappe.db.set_single_value("Healthcare Settings", "inpatient_visit_charge_item", item) + frappe.db.set_single_value("Healthcare Settings", "op_consulting_charge_item", item) appointment = frappe.new_doc("Patient Appointment") appointment.patient = patient appointment.practitioner = practitioner diff --git a/healthcare/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/healthcare/healthcare/doctype/patient_medical_record/test_patient_medical_record.py index f2d5dc7715..1440dd1913 100644 --- a/healthcare/healthcare/doctype/patient_medical_record/test_patient_medical_record.py +++ b/healthcare/healthcare/doctype/patient_medical_record/test_patient_medical_record.py @@ -18,8 +18,8 @@ class TestPatientMedicalRecord(FrappeTestCase): def setUp(self): - 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_single_value("Healthcare Settings", "enable_free_follow_ups", 0) + frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) make_pos_profile() def test_medical_record(self): From 7f23a19ce436d3e0a853b8bf4095fa9b2ab21614 Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Mon, 10 Jul 2023 13:32:36 +0530 Subject: [PATCH 15/39] fix: Utils - Return if no patient is selected on invoice submit/cancel (cherry picked from commit 1aac70289074a2e19d0d0c01542311bced4633d1) --- healthcare/healthcare/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index e1f36d4318..9821d0b1e0 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -488,6 +488,9 @@ def get_practitioner_charge(practitioner, is_inpatient): def manage_invoice_submit_cancel(doc, method): + if not doc.patient: + return + if doc.items: for item in doc.items: if item.get("reference_dt") and item.get("reference_dn"): From 9aa02edb8432969e542470d0e17bcfdffad1c936 Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Wed, 26 Apr 2023 12:34:22 +0530 Subject: [PATCH 16/39] fix: Enable Fee Validity for existing patients (cherry picked from commit 871d66361e9a91422da453ae06427c250e393079) # Conflicts: # healthcare/patches.txt --- .../doctype/fee_validity/fee_validity.json | 32 +++- .../doctype/fee_validity/fee_validity.py | 167 ++++++++++++++++-- .../doctype/fee_validity/fee_validity_list.js | 12 ++ .../doctype/fee_validity/test_fee_validity.py | 34 +++- .../patient_appointment.js | 75 +++++++- .../patient_appointment.py | 46 +++-- healthcare/healthcare/utils.py | 56 ++---- healthcare/hooks.py | 1 + healthcare/patches.txt | 5 + .../patches/v15_0/set_fee_validity_status.py | 8 + 10 files changed, 347 insertions(+), 89 deletions(-) create mode 100644 healthcare/healthcare/doctype/fee_validity/fee_validity_list.js create mode 100644 healthcare/patches/v15_0/set_fee_validity_status.py diff --git a/healthcare/healthcare/doctype/fee_validity/fee_validity.json b/healthcare/healthcare/doctype/fee_validity/fee_validity.json index f0cb684b26..c0b11a503c 100644 --- a/healthcare/healthcare/doctype/fee_validity/fee_validity.json +++ b/healthcare/healthcare/doctype/fee_validity/fee_validity.json @@ -2,7 +2,6 @@ "actions": [], "allow_copy": 1, "allow_import": 1, - "beta": 0, "creation": "2017-01-05 10:56:29.564806", "doctype": "DocType", "document_type": "Setup", @@ -11,9 +10,11 @@ "field_order": [ "practitioner", "patient", + "medical_department", "column_break_3", "status", - "section_break_5", + "patient_appointment", + "sales_invoice_ref", "section_break_3", "max_visits", "visited", @@ -81,7 +82,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "options": "Completed\nPending", + "options": "Active\nExpired\nCompleted\nCancelled", "read_only": 1 }, { @@ -99,14 +100,30 @@ "read_only": 1 }, { - "collapsible": 1, - "fieldname": "section_break_5", - "fieldtype": "Section Break" + "fieldname": "medical_department", + "fieldtype": "Link", + "label": "Medical Department", + "options": "Medical Department", + "read_only": 1 + }, + { + "fieldname": "sales_invoice_ref", + "fieldtype": "Link", + "label": "Sales Invoice Reference", + "options": "Sales Invoice", + "read_only": 1 + }, + { + "fieldname": "patient_appointment", + "fieldtype": "Link", + "label": "Patient Appointment", + "options": "Patient Appointment", + "read_only": 1 } ], "in_create": 1, "links": [], - "modified": "2021-08-26 10:51:05.609349", + "modified": "2023-04-09 10:40:36.311443", "modified_by": "Administrator", "module": "Healthcare", "name": "Fee Validity", @@ -130,5 +147,6 @@ "search_fields": "practitioner, patient", "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "practitioner" } \ No newline at end of file diff --git a/healthcare/healthcare/doctype/fee_validity/fee_validity.py b/healthcare/healthcare/doctype/fee_validity/fee_validity.py index 1fb0df74f4..4026744ad0 100644 --- a/healthcare/healthcare/doctype/fee_validity/fee_validity.py +++ b/healthcare/healthcare/doctype/fee_validity/fee_validity.py @@ -2,8 +2,8 @@ # Copyright (c) 2015, ESS LLP and contributors # For license information, please see license.txt - import datetime +import json import frappe from frappe.model.document import Document @@ -15,19 +15,26 @@ def validate(self): self.update_status() def update_status(self): - if self.visited >= self.max_visits: + if getdate(self.valid_till) < getdate(): + self.status = "Expired" + elif self.visited == self.max_visits: self.status = "Completed" else: - self.status = "Pending" + self.status = "Active" def create_fee_validity(appointment): - if not check_is_new_patient(appointment): + if patient_has_validity(appointment): return fee_validity = frappe.new_doc("Fee Validity") fee_validity.practitioner = appointment.practitioner fee_validity.patient = appointment.patient + fee_validity.medical_department = appointment.department + fee_validity.patient_appointment = appointment.name + fee_validity.sales_invoice_ref = frappe.db.get_value( + "Sales Invoice Item", {"reference_dn": appointment.name}, "parent" + ) fee_validity.max_visits = frappe.db.get_single_value("Healthcare Settings", "max_visits") or 1 valid_days = frappe.db.get_single_value("Healthcare Settings", "valid_days") or 1 fee_validity.visited = 0 @@ -39,22 +46,146 @@ def create_fee_validity(appointment): return fee_validity -def check_is_new_patient(appointment): +def patient_has_validity(appointment): validity_exists = frappe.db.exists( - "Fee Validity", {"practitioner": appointment.practitioner, "patient": appointment.patient} - ) - if validity_exists: - return False - - appointment_exists = frappe.db.get_all( - "Patient Appointment", + "Fee Validity", { - "name": ("!=", appointment.name), - "status": ("!=", "Cancelled"), - "patient": appointment.patient, "practitioner": appointment.practitioner, + "patient": appointment.patient, + "status": "Active", + "valid_till": [">=", appointment.appointment_date], + "start_date": ["<=", appointment.appointment_date], }, ) - if len(appointment_exists) and appointment_exists[0]: - return False - return True + + return validity_exists + + +@frappe.whitelist() +def check_fee_validity(appointment, date=None, practitioner=None): + if not frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups"): + return + + if isinstance(appointment, str): + appointment = json.loads(appointment) + appointment = frappe.get_doc(appointment) + + date = getdate(date) if date else appointment.appointment_date + + filters = { + "practitioner": practitioner if practitioner else appointment.practitioner, + "patient": appointment.patient, + "valid_till": (">=", date), + "start_date": ("<=", date), + } + if appointment.status != "Cancelled": + filters["status"] = "Active" + + validity = frappe.db.exists( + "Fee Validity", + filters, + ) + + if not validity: + # return valid fee validity when rescheduling appointment + if appointment.get("__islocal"): + return + else: + validity = get_fee_validity(appointment.get("name"), date) or None + if validity: + return validity + return + + validity = frappe.get_doc("Fee Validity", validity) + return validity + + +def manage_fee_validity(appointment): + free_follow_ups = frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") + # Update fee validity dates when rescheduling an invoiced appointment + if free_follow_ups: + invoiced_fee_validity = frappe.db.exists( + "Fee Validity", {"patient_appointment": appointment.name} + ) + if invoiced_fee_validity and appointment.invoiced: + start_date = frappe.db.get_value("Fee Validity", invoiced_fee_validity, "start_date") + if getdate(appointment.appointment_date) != start_date: + frappe.db.set_value( + "Fee Validity", + invoiced_fee_validity, + { + "start_date": appointment.appointment_date, + "valid_till": getdate(appointment.appointment_date) + + datetime.timedelta( + days=int(frappe.db.get_single_value("Healthcare Settings", "valid_days") or 1) + ), + }, + ) + + fee_validity = check_fee_validity(appointment) + + if fee_validity: + exists = frappe.db.exists("Fee Validity Reference", {"appointment": appointment.name}) + if appointment.status == "Cancelled" and fee_validity.visited > 0: + fee_validity.visited -= 1 + frappe.db.delete("Fee Validity Reference", {"appointment": appointment.name}) + elif fee_validity.status != "Active": + return + elif appointment.name != fee_validity.patient_appointment and not exists: + fee_validity.visited += 1 + fee_validity.append("ref_appointments", {"appointment": appointment.name}) + fee_validity.save(ignore_permissions=True) + else: + # remove appointment from fee validity reference when rescheduling an appointment to date not in fee validity + free_visit_validity = frappe.db.get_value( + "Fee Validity Reference", {"appointment": appointment.name}, "parent" + ) + if free_visit_validity: + fee_validity = frappe.get_doc( + "Fee Validity", + free_visit_validity, + ) + if fee_validity: + frappe.db.delete("Fee Validity Reference", {"appointment": appointment.name}) + if fee_validity.visited > 0: + fee_validity.visited -= 1 + fee_validity.save(ignore_permissions=True) + fee_validity = create_fee_validity(appointment) + return fee_validity + + +@frappe.whitelist() +def get_fee_validity(appointment_name, date): + """ + Get the fee validity details for the free visit appointment + :params appointment_name: Appointment doc name + :params date: Schedule date + :return fee validity name and valid_till values of free visit appointments + """ + if appointment_name: + appointment_doc = frappe.get_doc("Patient Appointment", appointment_name) + fee_validity = frappe.qb.DocType("Fee Validity") + child = frappe.qb.DocType("Fee Validity Reference") + + return ( + frappe.qb.from_(fee_validity) + .inner_join(child) + .on(fee_validity.name == child.parent) + .select(fee_validity.name, fee_validity.valid_till) + .where(fee_validity.status == "Active") + .where(fee_validity.start_date <= date) + .where(fee_validity.valid_till >= date) + .where(fee_validity.patient == appointment_doc.patient) + .where(fee_validity.practitioner == appointment_doc.practitioner) + .where(child.appointment == appointment_name) + ).run(as_dict=True) + + +def update_validity_status(): + # update the status of fee validity daily + validities = frappe.db.get_all("Fee Validity", {"status": ["not in", ["Expired", "Cancelled"]]}) + + for fee_validity in validities: + fee_validity_doc = frappe.get_doc("Fee Validity", fee_validity.name) + fee_validity_doc.update_status() + fee_validity_doc.save() diff --git a/healthcare/healthcare/doctype/fee_validity/fee_validity_list.js b/healthcare/healthcare/doctype/fee_validity/fee_validity_list.js new file mode 100644 index 0000000000..b897c2f842 --- /dev/null +++ b/healthcare/healthcare/doctype/fee_validity/fee_validity_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings['Fee Validity'] = { + add_fields: ['practitioner', 'patient', 'status', 'valid_till'], + get_indicator: function (doc) { + const color = { + 'Active': 'green', + 'Completed': 'green', + 'Expired': 'red', + 'Cancelled': 'red', + }; + return [__(doc.status), color[doc.status], 'status,=,' + doc.status]; + } +} \ No newline at end of file diff --git a/healthcare/healthcare/doctype/fee_validity/test_fee_validity.py b/healthcare/healthcare/doctype/fee_validity/test_fee_validity.py index 54ad0bb37d..a7e9d813b9 100644 --- a/healthcare/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/healthcare/healthcare/doctype/fee_validity/test_fee_validity.py @@ -12,6 +12,7 @@ create_appointment, create_healthcare_docs, create_healthcare_service_items, + update_status, ) test_dependencies = ["Company"] @@ -37,20 +38,47 @@ def test_fee_validity(self): # For first appointment, invoice is generated. First appointment not considered in fee validity appointment = create_appointment(patient, practitioner, nowdate()) + fee_validity = frappe.db.exists( + "Fee Validity", + {"patient": patient, "practitioner": practitioner, "patient_appointment": appointment.name}, + ) invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced") self.assertEqual(invoiced, 1) + self.assertTrue(fee_validity) + self.assertEqual(frappe.db.get_value("Fee Validity", fee_validity, "status"), "Active") # appointment should not be invoiced as it is within fee validity appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4)) invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced") self.assertEqual(invoiced, 0) - # appointment should be invoiced as it is within fee validity but the max_visits are exceeded + self.assertEqual(frappe.db.get_value("Fee Validity", fee_validity, "visited"), 1) + self.assertEqual(frappe.db.get_value("Fee Validity", fee_validity, "status"), "Completed") + + # appointment should be invoiced as it is within fee validity but the max_visits are exceeded, should insert new fee validity appointment = create_appointment(patient, practitioner, add_days(nowdate(), 5), invoice=1) invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced") self.assertEqual(invoiced, 1) - # appointment should be invoiced as it is not within fee validity and the max_visits are exceeded - appointment = create_appointment(patient, practitioner, add_days(nowdate(), 10), invoice=1) + fee_validity = frappe.db.exists( + "Fee Validity", + {"patient": patient, "practitioner": practitioner, "patient_appointment": appointment.name}, + ) + self.assertTrue(fee_validity) + self.assertEqual(frappe.db.get_value("Fee Validity", fee_validity, "status"), "Active") + + # appointment should be invoiced as it is not within fee validity and insert new fee validity + appointment = create_appointment(patient, practitioner, add_days(nowdate(), 13), invoice=1) invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced") self.assertEqual(invoiced, 1) + + fee_validity = frappe.db.exists( + "Fee Validity", + {"patient": patient, "practitioner": practitioner, "patient_appointment": appointment.name}, + ) + self.assertTrue(fee_validity) + self.assertEqual(frappe.db.get_value("Fee Validity", fee_validity, "status"), "Active") + + # For first appointment cancel should cancel fee validity + update_status(appointment.name, "Cancelled") + self.assertEqual(frappe.db.get_value("Fee Validity", fee_validity, "status"), "Cancelled") diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js index 2345727a36..8c254a556e 100644 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js @@ -310,8 +310,11 @@ let check_and_set_availability = function(frm) { { fieldtype: 'Column Break' }, { fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date', min_date: new Date(frappe.datetime.get_today()) }, { fieldtype: 'Section Break' }, - { fieldtype: 'HTML', fieldname: 'available_slots' } - + { fieldtype: 'HTML', fieldname: 'available_slots' }, + { fieldtype: 'Section Break', fieldname: 'payment_section', label: 'Payment Details', hidden: 1 }, + { fieldtype: 'Link', options: 'Mode of Payment', fieldname: 'mode_of_payment', label: 'Mode of Payment' }, + { fieldtype: 'Column Break' }, + { fieldtype: 'Currency', fieldname: 'consultation_charge', label: 'Consultation Charge', read_only: 1 }, ], primary_action_label: __('Book'), primary_action: function() { @@ -328,6 +331,9 @@ let check_and_set_availability = function(frm) { frm.set_value('practitioner', d.get_value('practitioner')); frm.set_value('department', d.get_value('department')); frm.set_value('appointment_date', d.get_value('appointment_date')); + if (d.get_value('mode_of_payment') != frm.doc.mode_of_payment) { + frm.set_value('mode_of_payment', d.get_value('mode_of_payment')); + }; if (service_unit) { frm.set_value('service_unit', service_unit); @@ -343,7 +349,8 @@ let check_and_set_availability = function(frm) { d.set_values({ 'department': frm.doc.department, 'practitioner': frm.doc.practitioner, - 'appointment_date': frm.doc.appointment_date + 'appointment_date': frm.doc.appointment_date, + 'mode_of_payment': frm.doc.mode_of_payment, }); let selected_department = frm.doc.department; @@ -375,17 +382,57 @@ let check_and_set_availability = function(frm) { d.fields_dict['appointment_date'].df.onchange = () => { show_slots(d, fd); + validate_fee_validity(frm, d); }; d.fields_dict['practitioner'].df.onchange = () => { if (d.get_value('practitioner') && d.get_value('practitioner') != selected_practitioner) { selected_practitioner = d.get_value('practitioner'); show_slots(d, fd); + validate_fee_validity(frm, d); } }; d.show(); } + function validate_fee_validity(frm, d) { + var section_field = d.get_field("payment_section"); + var payment_field = d.get_field("mode_of_payment"); + section_field.df.hidden = 1; + payment_field.df.reqd = 0; + + if (d.get_value('appointment_date') && !frm.doc.invoiced) { + frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups').then(async function (val) { + if (val) { + fee_validity = (await frappe.call( + 'healthcare.healthcare.doctype.fee_validity.fee_validity.check_fee_validity', + { + appointment: frm.doc, + date: d.get_value('appointment_date'), + practitioner: d.get_value('practitioner') + } + )).message || null; + if (!fee_validity) { + payment_field.df.reqd = 1; + section_field.df.hidden = 0; + + let payment_details = (await frappe.call( + 'healthcare.healthcare.utils.get_service_item_and_practitioner_charge', + { + doc: frm.doc + } + )).message; + d.set_value('consultation_charge', payment_details.practitioner_charge); + payment_field.refresh(); + section_field.refresh(); + } + } + }); + } + payment_field.refresh(); + section_field.refresh(); + } + function show_slots(d, fd) { if (d.get_value('appointment_date') && d.get_value('practitioner')) { fd.available_slots.html(''); @@ -393,7 +440,8 @@ let check_and_set_availability = function(frm) { method: 'healthcare.healthcare.doctype.patient_appointment.patient_appointment.get_availability_data', args: { practitioner: d.get_value('practitioner'), - date: d.get_value('appointment_date') + date: d.get_value('appointment_date'), + appointment: frm.doc }, callback: (r) => { let data = r.message; @@ -401,7 +449,7 @@ let check_and_set_availability = function(frm) { let $wrapper = d.fields_dict.available_slots.$wrapper; // make buttons for each slot - let slot_html = get_slots(data.slot_details, d.get_value('appointment_date')); + let slot_html = get_slots(data.slot_details, data.fee_validity, d.get_value('appointment_date')); $wrapper .css('margin-bottom', 0) @@ -466,14 +514,27 @@ let check_and_set_availability = function(frm) { } } - function get_slots(slot_details, appointment_date) { + function get_slots(slot_details, fee_validity, appointment_date) { 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 += `
+ slot_html += `
`; + if (fee_validity && fee_validity != 'Disabled') { + slot_html += ` + + ${__('Patient has fee validity till')} ${moment(fee_validity.valid_till).format('DD-MM-YYYY')} +
`; + } else if (fee_validity != 'Disabled') { + slot_html += ` + + ${__('Patient has no fee validity, need to be invoiced')} +
`; + } + + slot_html += ` ${__('Practitioner Schedule: ')} ${slot_info.slot_name} ${slot_info.tele_conf && !slot_info.allow_overlap ? '' : ''} diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py index fc048e2dde..3cc65a1031 100755 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py @@ -14,15 +14,16 @@ from frappe.model.mapper import get_mapped_doc from frappe.utils import flt, format_date, get_link_to_form, get_time, getdate +from healthcare.healthcare.doctype.fee_validity.fee_validity import ( + check_fee_validity, + get_fee_validity, + manage_fee_validity, +) from healthcare.healthcare.doctype.healthcare_settings.healthcare_settings import ( get_income_account, get_receivable_account, ) -from healthcare.healthcare.utils import ( - check_fee_validity, - get_service_item_and_practitioner_charge, - manage_fee_validity, -) +from healthcare.healthcare.utils import get_service_item_and_practitioner_charge class MaximumCapacityError(frappe.ValidationError): @@ -43,11 +44,13 @@ def validate(self): self.set_title() self.update_event() + def on_update(self): + invoice_appointment(self) + self.update_fee_validity() + def after_insert(self): self.update_prescription_details() self.set_payment_details() - invoice_appointment(self) - self.update_fee_validity() send_confirmation_msg(self) self.insert_calendar_event() @@ -293,7 +296,7 @@ def check_payment_fields_reqd(patient): free_follow_ups = frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") if automate_invoicing: if free_follow_ups: - fee_validity = frappe.db.exists("Fee Validity", {"patient": patient, "status": "Pending"}) + fee_validity = frappe.db.exists("Fee Validity", {"patient": patient, "status": "Active"}) if fee_validity: return {"fee_validity": fee_validity} return True @@ -312,10 +315,11 @@ def invoice_appointment(appointment_doc): ) if enable_free_follow_ups: fee_validity = check_fee_validity(appointment_doc) - if fee_validity and fee_validity.status == "Completed": + + if fee_validity and fee_validity.status != "Active": fee_validity = None elif not fee_validity: - if frappe.db.exists("Fee Validity Reference", {"appointment": appointment_doc.name}): + if get_fee_validity(appointment_doc.name, appointment_doc.appointment_date): return else: fee_validity = None @@ -392,6 +396,11 @@ def cancel_appointment(appointment_id): msg = _("Appointment Cancelled. Please review and cancel the invoice {0}").format( sales_invoice.name ) + if frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups"): + fee_validity = frappe.db.get_value("Fee Validity", {"patient_appointment": appointment.name}) + if fee_validity: + frappe.db.set_value("Fee Validity", fee_validity, "status", "Cancelled") + else: fee_validity = manage_fee_validity(appointment) msg = _("Appointment Cancelled.") @@ -428,7 +437,7 @@ def check_sales_invoice_exists(appointment): @frappe.whitelist() -def get_availability_data(date, practitioner): +def get_availability_data(date, practitioner, appointment): """ Get availability data of 'practitioner' on 'date' :param date: Date to check in schedule @@ -459,7 +468,20 @@ def get_availability_data(date, practitioner): _("Healthcare Practitioner not available on {0}").format(weekday), title=_("Not Available") ) - return {"slot_details": slot_details} + if isinstance(appointment, str): + appointment = json.loads(appointment) + appointment = frappe.get_doc(appointment) + + fee_validity = "Disabled" + if frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups"): + fee_validity = check_fee_validity(appointment, date, practitioner) + if not fee_validity and not appointment.get("__islocal"): + fee_validity = get_fee_validity(appointment.get("name"), date) or None + + if appointment.invoiced: + fee_validity = "Disabled" + + return {"slot_details": slot_details, "fee_validity": fee_validity} def check_employee_wise_availability(date, practitioner_doc): diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index 9821d0b1e0..0d09171149 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -12,7 +12,6 @@ from frappe.utils import cstr, get_link_to_form, rounded, time_diff_in_hours from frappe.utils.formatters import format_value -from healthcare.healthcare.doctype.fee_validity.fee_validity import create_fee_validity from healthcare.healthcare.doctype.healthcare_settings.healthcare_settings import ( get_income_account, ) @@ -497,10 +496,20 @@ def manage_invoice_submit_cancel(doc, method): if frappe.get_meta(item.reference_dt).has_field("invoiced"): set_invoiced(item, method, doc.name) - if method == "on_submit" and frappe.db.get_single_value( - "Healthcare Settings", "create_lab_test_on_si_submit" - ): - create_multiple("Sales Invoice", doc.name) + if method == "on_submit": + if frappe.db.get_single_value("Healthcare Settings", "create_lab_test_on_si_submit"): + create_multiple("Sales Invoice", doc.name) + + if ( + not frappe.db.get_single_value("Healthcare Settings", "automate_appointment_invoicing") + and frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") + and doc.items + ): + for item in doc.items: + if item.reference_dt == "Patient Appointment": + fee_validity = frappe.db.exists("Fee Validity", {"patient_appointment": item.reference_dn}) + if fee_validity: + frappe.db.set_value("Fee Validity", fee_validity, "sales_invoice_ref", doc.name) def set_invoiced(item, method, ref_invoice=None): @@ -563,43 +572,6 @@ def manage_prescriptions(invoiced, ref_dt, ref_dn, dt, created_check_field): frappe.db.set_value(dt, doc_created, "invoiced", invoiced) -def check_fee_validity(appointment): - if not frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups"): - return - - validity = frappe.db.exists( - "Fee Validity", - { - "practitioner": appointment.practitioner, - "patient": appointment.patient, - "valid_till": (">=", appointment.appointment_date), - }, - ) - if not validity: - return - - validity = frappe.get_doc("Fee Validity", validity) - return validity - - -def manage_fee_validity(appointment): - fee_validity = check_fee_validity(appointment) - - if fee_validity: - if appointment.status == "Cancelled" and fee_validity.visited > 0: - fee_validity.visited -= 1 - frappe.db.delete("Fee Validity Reference", {"appointment": appointment.name}) - elif fee_validity.status == "Completed": - return - else: - fee_validity.visited += 1 - fee_validity.append("ref_appointments", {"appointment": appointment.name}) - fee_validity.save(ignore_permissions=True) - else: - fee_validity = create_fee_validity(appointment) - return fee_validity - - def manage_doc_for_appointment(dt_from_appointment, appointment, invoiced): dn_from_appointment = frappe.db.get_value( dt_from_appointment, filters={"appointment": appointment} diff --git a/healthcare/hooks.py b/healthcare/hooks.py index 0db002babe..2112111e33 100644 --- a/healthcare/hooks.py +++ b/healthcare/hooks.py @@ -132,6 +132,7 @@ ], "daily": [ "healthcare.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status", + "healthcare.healthcare.doctype.fee_validity.fee_validity.update_validity_status", ], } diff --git a/healthcare/patches.txt b/healthcare/patches.txt index ebaca082eb..167eca243c 100644 --- a/healthcare/patches.txt +++ b/healthcare/patches.txt @@ -1,2 +1,7 @@ healthcare.patches.v0_0.setup_abdm_custom_fields healthcare.patches.v0_0.set_medical_code_from_field_to_codification_table +<<<<<<< HEAD +======= +healthcare.patches.v15_0.check_version_compatibility_with_frappe +healthcare.patches.v15_0.set_fee_validity_status +>>>>>>> 871d663 (fix: Enable Fee Validity for existing patients) diff --git a/healthcare/patches/v15_0/set_fee_validity_status.py b/healthcare/patches/v15_0/set_fee_validity_status.py new file mode 100644 index 0000000000..a2b245eae8 --- /dev/null +++ b/healthcare/patches/v15_0/set_fee_validity_status.py @@ -0,0 +1,8 @@ +import frappe + + +def execute(): + validities = frappe.db.get_all("Fee Validity", {"status": "Pending"}, as_list=0) + + for fee_validity in validities: + frappe.db.set_value("Fee Validity", fee_validity, "status", "Active") From b3f6008071f8552ce8f7b69c141587f83972f540 Mon Sep 17 00:00:00 2001 From: Akash Krishna Date: Fri, 15 Sep 2023 11:44:18 +0530 Subject: [PATCH 17/39] fix: Update patches.txt --- healthcare/patches.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/healthcare/patches.txt b/healthcare/patches.txt index 167eca243c..806e8cb203 100644 --- a/healthcare/patches.txt +++ b/healthcare/patches.txt @@ -1,7 +1,3 @@ healthcare.patches.v0_0.setup_abdm_custom_fields healthcare.patches.v0_0.set_medical_code_from_field_to_codification_table -<<<<<<< HEAD -======= -healthcare.patches.v15_0.check_version_compatibility_with_frappe healthcare.patches.v15_0.set_fee_validity_status ->>>>>>> 871d663 (fix: Enable Fee Validity for existing patients) From 8f88f9e192e49757d5fe7dda17cfb350b63ab647 Mon Sep 17 00:00:00 2001 From: Akash Date: Fri, 21 Jul 2023 15:17:46 +0530 Subject: [PATCH 18/39] fix: book Patient Appointment based on check in (cherry picked from commit 291095dcfc42092c0466be74d4b3d209e05f7748) # Conflicts: # healthcare/healthcare/utils.py # healthcare/patches.txt --- .../appointment_type/appointment_type.js | 41 ++- .../appointment_type/appointment_type.json | 27 +- .../appointment_type/appointment_type.py | 42 +-- .../appointment_type_service_item.json | 33 +- .../healthcare_practitioner.py | 19 +- .../test_healthcare_practitioner.py | 64 +++- .../healthcare_schedule_time_slot.json | 185 +++------- .../patient_appointment.js | 320 ++++++++++++------ .../patient_appointment.json | 82 ++++- .../patient_appointment.py | 108 +++++- .../patient_appointment_list.js | 4 +- .../test_patient_appointment.py | 174 +++++++++- .../practitioner_schedule.js | 157 ++++++--- .../practitioner_schedule.py | 17 +- healthcare/healthcare/utils.py | 84 +++-- healthcare/patches.txt | 9 + ...artment_in_appoitment_type_service_item.py | 5 + ...nk_dt_for_appointment_type_service_item.py | 9 + healthcare/public/js/sales_invoice.js | 1 + 19 files changed, 973 insertions(+), 408 deletions(-) create mode 100644 healthcare/patches/v15_0/rename_field_medical_department_in_appoitment_type_service_item.py create mode 100644 healthcare/patches/v15_0/set_default_dynamic_link_dt_for_appointment_type_service_item.py diff --git a/healthcare/healthcare/doctype/appointment_type/appointment_type.js b/healthcare/healthcare/doctype/appointment_type/appointment_type.js index 99b7cb295a..16d7f3f9ec 100644 --- a/healthcare/healthcare/doctype/appointment_type/appointment_type.js +++ b/healthcare/healthcare/doctype/appointment_type/appointment_type.js @@ -9,13 +9,40 @@ frappe.ui.form.on('Appointment Type', { }; }); - frm.set_query('medical_department', 'items', function(doc) { - let item_list = doc.items.map(({medical_department}) => medical_department); - return { - filters: [ - ['Medical Department', 'name', 'not in', item_list] - ] - }; + frm.set_query('dt', 'items', function() { + if (['Department', 'Practitioner'].includes(frm.doc.allow_booking_for)) { + return { + filters: {'name': ['=', 'Medical Department']} + }; + } else if (frm.doc.allow_booking_for === "Service Unit") { + return { + filters: {'name': ['=', 'Healthcare Service Unit']} + }; + } + }); + + frm.set_query('dn', 'items', function(doc, cdt, cdn) { + let child = locals[cdt][cdn]; + if (child.dt === 'Medical Department') { + let item_list = doc.items + .filter(item => item.dt === 'Medical Department') + .map(({dn}) => dn); + return { + filters: [ + ['Medical Department', 'name', 'not in', item_list] + ] + }; + } else if (child.dt === 'Healthcare Service Unit') { + let item_list = doc.items + .filter(item => item.dt === 'Healthcare Service Unit') + .map(({dn}) => dn); + return { + filters: [ + ['Healthcare Service Unit', 'name', 'not in', item_list], + ['Healthcare Service Unit', 'allow_appointments', "=", 1], + ] + }; + } }); frm.set_query('op_consulting_charge_item', 'items', function() { diff --git a/healthcare/healthcare/doctype/appointment_type/appointment_type.json b/healthcare/healthcare/doctype/appointment_type/appointment_type.json index 75c9395533..b7109a2dd2 100644 --- a/healthcare/healthcare/doctype/appointment_type/appointment_type.json +++ b/healthcare/healthcare/doctype/appointment_type/appointment_type.json @@ -10,11 +10,10 @@ "field_order": [ "appointment_type", "default_duration", + "allow_booking_for", "column_break_jdo1", "color", - "ip", "billing_tab", - "billing_section", "price_list", "items" ], @@ -30,15 +29,6 @@ "translatable": 1, "unique": 1 }, - { - "bold": 1, - "default": "0", - "fieldname": "ip", - "fieldtype": "Check", - "label": "Is Inpatient", - "print_hide": 1, - "report_hide": 1 - }, { "allow_in_quick_entry": 1, "bold": 1, @@ -57,11 +47,6 @@ "no_copy": 1, "report_hide": 1 }, - { - "fieldname": "billing_section", - "fieldtype": "Section Break", - "label": "Billing" - }, { "fieldname": "price_list", "fieldtype": "Link", @@ -82,10 +67,18 @@ { "fieldname": "column_break_jdo1", "fieldtype": "Column Break" + }, + { + "default": "Practitioner", + "fieldname": "allow_booking_for", + "fieldtype": "Select", + "label": "Allow Booking For", + "options": "Practitioner\nDepartment\nService Unit", + "reqd": 1 } ], "links": [], - "modified": "2023-01-13 20:02:20.353136", + "modified": "2023-06-10 13:43:27.137414", "modified_by": "Administrator", "module": "Healthcare", "name": "Appointment Type", diff --git a/healthcare/healthcare/doctype/appointment_type/appointment_type.py b/healthcare/healthcare/doctype/appointment_type/appointment_type.py index ca73847e36..afdce66cac 100644 --- a/healthcare/healthcare/doctype/appointment_type/appointment_type.py +++ b/healthcare/healthcare/doctype/appointment_type/appointment_type.py @@ -32,26 +32,18 @@ def validate(self): ) -@frappe.whitelist() -def get_service_item_based_on_department(appointment_type, department): - item_list = frappe.db.get_value( - "Appointment Type Service Item", - filters={"medical_department": department, "parent": appointment_type}, - fieldname=[ - "op_consulting_charge_item", - "inpatient_visit_charge_item", - "op_consulting_charge", - "inpatient_visit_charge", - ], - as_dict=1, - ) - - # if department wise items are not set up - # use the generic items - if not item_list: - item_list = frappe.db.get_value( +def get_billing_details(appointment_type, docname=None): + def get_details(filters=None): + if not filters: + # fetch generic ones without department / service_unit + filters = { + "parent": appointment_type, + "dt": None, + "dn": None, + } + return frappe.db.get_value( "Appointment Type Service Item", - filters={"parent": appointment_type}, + filters=filters, fieldname=[ "op_consulting_charge_item", "inpatient_visit_charge_item", @@ -61,7 +53,17 @@ def get_service_item_based_on_department(appointment_type, department): as_dict=1, ) - return item_list + filters = { + "parent": appointment_type, + "dn": docname, + } + details = get_details(filters) + + # if department wise items are not set up + # use the generic items + if not details: + details = get_details() + return details def make_item_price(price_list, item, item_price): diff --git a/healthcare/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json b/healthcare/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json index ccae129ea0..97283f93f3 100644 --- a/healthcare/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json +++ b/healthcare/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json @@ -5,7 +5,8 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "medical_department", + "dt", + "dn", "op_consulting_charge_item", "op_consulting_charge", "column_break_4", @@ -14,13 +15,7 @@ ], "fields": [ { - "fieldname": "medical_department", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Medical Department", - "options": "Medical Department" - }, - { + "columns": 2, "fieldname": "op_consulting_charge_item", "fieldtype": "Link", "in_list_view": 1, @@ -28,6 +23,7 @@ "options": "Item" }, { + "columns": 1, "fieldname": "op_consulting_charge", "fieldtype": "Currency", "in_list_view": 1, @@ -38,6 +34,7 @@ "fieldtype": "Column Break" }, { + "columns": 2, "fieldname": "inpatient_visit_charge_item", "fieldtype": "Link", "in_list_view": 1, @@ -45,16 +42,33 @@ "options": "Item" }, { + "columns": 1, "fieldname": "inpatient_visit_charge", "fieldtype": "Currency", "in_list_view": 1, "label": "Inpatient Visit Charge" + }, + { + "columns": 2, + "fieldname": "dt", + "fieldtype": "Link", + "in_list_view": 1, + "label": "DocType", + "options": "DocType" + }, + { + "columns": 2, + "fieldname": "dn", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "DocName", + "options": "dt" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-17 06:05:02.240812", + "modified": "2023-07-02 07:55:17.926324", "modified_by": "Administrator", "module": "Healthcare", "name": "Appointment Type Service Item", @@ -63,5 +77,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/healthcare/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py b/healthcare/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py index a29d045155..9a0dc6d45c 100644 --- a/healthcare/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py +++ b/healthcare/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py @@ -34,11 +34,26 @@ def validate(self): self.inpatient_visit_charge_item, "Configure a service Item for Inpatient Consulting Charge Item", ) + if not self.inpatient_visit_charge: + frappe.throw( + _( + "Inpatient Consulting Charge is mandatory if you are setting Inpatient Consulting Charge Item" + ), + frappe.MandatoryError, + ) + if self.op_consulting_charge_item: validate_service_item( self.op_consulting_charge_item, - "Configure a service Item for Out Patient Consulting Charge Item", + "Configure a service Item for Outpatient Consulting Charge Item", ) + if not self.op_consulting_charge: + frappe.throw( + _( + "Outpatient Consulting Charge is mandatory if you are setting Outpatient Consulting Charge Item" + ), + frappe.MandatoryError, + ) if self.user_id: self.validate_user_id() @@ -106,7 +121,7 @@ def on_trash(self): def validate_service_item(item, msg): if frappe.db.get_value("Item", item, "is_stock_item"): - frappe.throw(_(msg)) + frappe.throw(_(msg), frappe.ValidationError) @frappe.whitelist() diff --git a/healthcare/healthcare/doctype/healthcare_practitioner/test_healthcare_practitioner.py b/healthcare/healthcare/doctype/healthcare_practitioner/test_healthcare_practitioner.py index f5ca5d9cc1..9873f1eddf 100644 --- a/healthcare/healthcare/doctype/healthcare_practitioner/test_healthcare_practitioner.py +++ b/healthcare/healthcare/doctype/healthcare_practitioner/test_healthcare_practitioner.py @@ -2,9 +2,69 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt - +import frappe from frappe.tests.utils import FrappeTestCase class TestHealthcarePractitioner(FrappeTestCase): - pass + def test_practitioner_mandatory_charges(self): + fieldnames = ["op_consulting_charge", "inpatient_visit_charge"] + for idx, fieldname in enumerate(fieldnames): + item_fieldname = f"{fieldname}_item" + charge_fieldname = f"{fieldname}" + practitioner = frappe.get_doc( + { + "doctype": "Healthcare Practitioner", + "first_name": f"__Test Healthcare Practitioner {idx}", + "gender": "Female", + item_fieldname: self.get_item(is_stock_item=False), + charge_fieldname: 0, + } + ) + self.assertRaises(frappe.MandatoryError, practitioner.insert) + + def test_practitioner_service_item(self): + fieldnames = ["op_consulting_charge", "inpatient_visit_charge"] + for idx, fieldname in enumerate(fieldnames): + item_fieldname = f"{fieldname}_item" + charge_fieldname = f"{fieldname}" + practitioner = frappe.get_doc( + { + "doctype": "Healthcare Practitioner", + "first_name": f"__Test Healthcare Practitioner {idx}", + "gender": "Male", + item_fieldname: self.get_item(is_stock_item=True), + charge_fieldname: 0, + } + ) + self.assertRaises(frappe.ValidationError, practitioner.insert) + + def get_item(self, is_stock_item=False): + item_code = "__Test Stock Item" if is_stock_item else "__Test Service Item" + + if not frappe.db.exists("Item", item_code): + return ( + frappe.get_doc( + { + "doctype": "Item", + "name": item_code, + "item_code": item_code, + "item_name": item_code, + "is_stock_item": is_stock_item, + "item_group": "All Item Groups", + "stock_uom": "Nos", + } + ) + .insert() + .name + ) + else: + return item_code + + @classmethod + def tearDown(cls): + frappe.delete_doc_if_exists("Item", "__Test Stock Item", force=True) + frappe.delete_doc_if_exists("Item", "__Test Service Item", force=True) + frappe.db.sql( + """delete from `tabHealthcare Practitioner` where name like '__Test Healthcare Practitioner%'""" + ) diff --git a/healthcare/healthcare/doctype/healthcare_schedule_time_slot/healthcare_schedule_time_slot.json b/healthcare/healthcare/doctype/healthcare_schedule_time_slot/healthcare_schedule_time_slot.json index 23318795e7..007c7dd1b3 100644 --- a/healthcare/healthcare/doctype/healthcare_schedule_time_slot/healthcare_schedule_time_slot.json +++ b/healthcare/healthcare/doctype/healthcare_schedule_time_slot/healthcare_schedule_time_slot.json @@ -1,136 +1,63 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-05-03 17:27:07.466088", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-05-03 17:27:07.466088", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "day", + "from_time", + "to_time", + "duration", + "maximum_appointments" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "day", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Day", - "length": 0, - "no_copy": 0, - "options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "day", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Day", + "options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "from_time", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "From Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "from_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "From Time", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_time", - "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "To Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "to_time", + "fieldtype": "Time", + "in_list_view": 1, + "label": "To Time", + "reqd": 1 + }, + { + "fieldname": "maximum_appointments", + "fieldtype": "Int", + "label": "Maximum Appointments" + }, + { + "fieldname": "duration", + "fieldtype": "Float", + "label": "Duration", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2020-09-18 17:26:09.703215", - "modified_by": "Administrator", - "module": "Healthcare", - "name": "Healthcare Schedule Time Slot", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Healthcare", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2023-07-21 15:01:12.835381", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Healthcare Schedule Time Slot", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "restrict_to_domain": "Healthcare", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js index 8c254a556e..4a99cefc11 100644 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js @@ -38,7 +38,8 @@ frappe.ui.form.on('Patient Appointment', { query: 'healthcare.controllers.queries.get_healthcare_service_units', filters: { company: frm.doc.company, - inpatient_record: frm.doc.inpatient_record + inpatient_record: frm.doc.inpatient_record, + allow_appointments: 1, } }; }); @@ -54,43 +55,10 @@ frappe.ui.form.on('Patient Appointment', { frm.trigger('set_therapy_type_filter'); if (frm.is_new()) { - frm.page.set_primary_action(__('Check Availability'), function() { - if (!frm.doc.patient) { - frappe.msgprint({ - title: __('Not Allowed'), - message: __('Please select Patient first'), - indicator: 'red' - }); - } else { - frappe.call({ - method: 'healthcare.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd', - args: { 'patient': frm.doc.patient }, - callback: function(data) { - if (data.message == true) { - if (frm.doc.mode_of_payment && frm.doc.paid_amount) { - check_and_set_availability(frm); - } - if (!frm.doc.mode_of_payment) { - frappe.msgprint({ - title: __('Not Allowed'), - message: __('Please select a Mode of Payment first'), - indicator: 'red' - }); - } - if (!frm.doc.paid_amount) { - frappe.msgprint({ - title: __('Not Allowed'), - message: __('Please set the Paid Amount first'), - indicator: 'red' - }); - } - } else { - check_and_set_availability(frm); - } - } - }); - } - }); + frm.page.clear_primary_action(); + if (frm.doc.appointment_for) { + frm.trigger('appointment_for'); + } } else { frm.page.set_primary_action(__('Save'), () => frm.save()); } @@ -102,7 +70,7 @@ frappe.ui.form.on('Patient Appointment', { }, __('View')); } - if (frm.doc.status == 'Open' || (frm.doc.status == 'Scheduled' && !frm.doc.__islocal)) { + if (["Open", "Checked In"].includes(frm.doc.status) || (frm.doc.status == 'Scheduled' && !frm.doc.__islocal)) { frm.add_custom_button(__('Cancel'), function() { update_status(frm, 'Cancelled'); }); @@ -137,11 +105,101 @@ frappe.ui.form.on('Patient Appointment', { create_vital_signs(frm); }, __('Create')); } + + if (!frm.doc.__islocal && frm.doc.status=="Open" && frm.doc.appointment_based_on_check_in) { + frm.add_custom_button(__('Check In'), () => { + frm.set_value("status", "Checked In"); + frm.save(); + }); + } + }, + + appointment_for: function(frm) { + if (frm.doc.appointment_for == 'Practitioner') { + if (!frm.doc.practitioner) { + frm.set_value('department', ''); + } + frm.set_value('service_unit', ''); + frm.trigger('set_check_availability_action'); + } else if (frm.doc.appointment_for == 'Service Unit') { + frm.set_value({ + 'practitioner': '', + 'practitioner_name': '', + 'department': '', + }); + frm.trigger('set_book_action'); + } else if (frm.doc.appointment_for == 'Department') { + frm.set_value({ + 'practitioner': '', + 'practitioner_name': '', + 'service_unit': '', + }); + frm.trigger('set_book_action'); + } else { + if (frm.doc.appointment_for == 'Department') { + frm.set_value('service_unit', ''); + } + frm.set_value({ + 'practitioner': '', + 'practitioner_name': '', + 'department': '', + 'service_unit': '', + }); + frm.page.clear_primary_action(); + } + }, + + set_book_action: function(frm) { + frm.page.set_primary_action(__('Book'), function() { + frm.enable_save(); + frm.save(); + }); + }, + + set_check_availability_action: function(frm) { + frm.page.set_primary_action(__('Check Availability'), function() { + if (!frm.doc.patient) { + frappe.msgprint({ + title: __('Not Allowed'), + message: __('Please select Patient first'), + indicator: 'red' + }); + } else { + frappe.call({ + method: 'healthcare.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd', + args: { 'patient': frm.doc.patient }, + callback: function(data) { + if (data.message == true) { + if (frm.doc.mode_of_payment && frm.doc.paid_amount) { + check_and_set_availability(frm); + } + if (!frm.doc.mode_of_payment) { + frappe.msgprint({ + title: __('Not Allowed'), + message: __('Please select a Mode of Payment first'), + indicator: 'red' + }); + } + if (!frm.doc.paid_amount) { + frappe.msgprint({ + title: __('Not Allowed'), + message: __('Please set the Paid Amount first'), + indicator: 'red' + }); + } + } else { + check_and_set_availability(frm); + } + } + }); + } + }); }, patient: function(frm) { if (frm.doc.patient) { frm.trigger('toggle_payment_fields'); + frm.trigger('appointment_for'); frappe.call({ method: 'frappe.client.get', args: { @@ -172,6 +230,20 @@ frappe.ui.form.on('Patient Appointment', { appointment_type: function(frm) { if (frm.doc.appointment_type) { + if (frm.doc.appointment_for && frm.doc[frappe.scrub(frm.doc.appointment_for)]) { + frm.events.set_payment_details(frm); + } + } + }, + + department: function(frm) { + if (frm.doc.department && frm.doc.appointment_for == 'Department') { + frm.events.set_payment_details(frm); + } + }, + + service_unit: function(frm) { + if (frm.doc.service_unit && frm.doc.appointment_for == 'Service Unit') { frm.events.set_payment_details(frm); } }, @@ -180,7 +252,7 @@ frappe.ui.form.on('Patient Appointment', { frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing').then(val => { if (val) { frappe.call({ - method: 'healthcare.healthcare.utils.get_service_item_and_practitioner_charge', + method: 'healthcare.healthcare.utils.get_appointment_billing_item_and_rate', args: { doc: frm.doc }, @@ -288,6 +360,7 @@ let check_and_set_availability = function(frm) { let duration = null; let add_video_conferencing = null; let overlap_appointments = null; + let appointment_based_on_check_in = false; show_availability(); @@ -323,7 +396,6 @@ let check_and_set_availability = function(frm) { && !overlap_appointments frm.set_value('add_video_conferencing', add_video_conferencing); - if (!frm.doc.duration) { frm.set_value('duration', duration); } @@ -334,6 +406,7 @@ let check_and_set_availability = function(frm) { if (d.get_value('mode_of_payment') != frm.doc.mode_of_payment) { frm.set_value('mode_of_payment', d.get_value('mode_of_payment')); }; + frm.set_value('appointment_based_on_check_in', appointment_based_on_check_in) if (service_unit) { frm.set_value('service_unit', service_unit); @@ -417,7 +490,7 @@ let check_and_set_availability = function(frm) { section_field.df.hidden = 0; let payment_details = (await frappe.call( - 'healthcare.healthcare.utils.get_service_item_and_practitioner_charge', + 'healthcare.healthcare.utils.get_appointment_billing_item_and_rate', { doc: frm.doc } @@ -463,6 +536,7 @@ let check_and_set_availability = function(frm) { $btn.addClass('btn-outline-primary'); selected_slot = $btn.attr('data-name'); service_unit = $btn.attr('data-service-unit'); + appointment_based_on_check_in = $btn.attr('data-day-appointment'); duration = $btn.attr('data-duration'); add_video_conferencing = parseInt($btn.attr('data-tele-conf')); overlap_appointments = parseInt($btn.attr('data-overlap-appointments')); @@ -540,85 +614,111 @@ let check_and_set_availability = function(frm) { ${slot_info.tele_conf && !slot_info.allow_overlap ? '' : ''}
${__('Service Unit: ')} ${slot_info.service_unit}`; + if (slot_info.service_unit_capacity) { + slot_html += `
${__('Maximum Capacity:')} ${slot_info.service_unit_capacity} `; + } - if (slot_info.service_unit_capacity) { - slot_html += `
${__('Maximum Capacity:')} ${slot_info.service_unit_capacity} `; - } + slot_html += '

'; + + slot_html += slot_info.avail_slot.map(slot => { + appointment_count = 0; + disabled = false; + count_class = tool_tip = ''; + 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; + + // restrict past slots based on the current time. + let now = moment(); + let booked_moment = "" + if((now.format("YYYY-MM-DD") == appointment_date) && (slot_start_time.isBefore(now) && !slot.maximum_appointments)){ + disabled = true; + } else { + // iterate in all booked appointments, update the start time and duration + slot_info.appointments.forEach((booked) => { + booked_moment = moment(booked.appointment_time, 'HH:mm:ss'); + let end_time = booked_moment.clone().add(booked.duration, 'minutes'); + + // to get apointment count for all day appointments + if (slot.maximum_appointments) { + if (booked.appointment_date == appointment_date) { + appointment_count++; + } + } + // 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; + } + } - slot_html += '

'; - - slot_html += slot_info.avail_slot.map(slot => { - appointment_count = 0; - disabled = false; - count_class = tool_tip = ''; - 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; - - // restrict past slots based on the current time. - let now = moment(); - if((now.format("YYYY-MM-DD") == appointment_date) && slot_start_time.isBefore(now)){ - disabled = true; - } else { - // 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')}`; } - // 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 + if (slot.maximum_appointments) { + if (appointment_count >= slot.maximum_appointments) { disabled = true; - return false; } - } else { - if (slot_start_time.isBefore(end_time) && slot_end_time.isAfter(booked_moment)) { - appointment_count++; + else { + disabled = false; } - 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')}`; - } + available_slots = slot.maximum_appointments - appointment_count; + count = `${(available_slots > 0 ? available_slots : __('Full'))}`; + count_class = `${(available_slots > 0 ? 'badge-success' : 'badge-danger')}`; + return `` + } else { - return ` - `; + return ` + `; + } }).join(""); - if (slot_info.service_unit_capacity) { - slot_html += `
${__('Each slot indicates the capacity currently available for booking')}`; - } - slot_html += `

`; + if (slot_info.service_unit_capacity) { + slot_html += `
${__('Each slot indicates the capacity currently available for booking')}`; + } + slot_html += `

`; + }); return slot_html; diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.json b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.json index a002a86e53..23d29ca7e8 100644 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.json @@ -2,8 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "naming_series:", - "beta": 0, - "creation": "2017-05-04 11:52:40.941507", + "creation": "2017-05-04 11:52:41.141507", "doctype": "DocType", "document_type": "Document", "engine": "InnoDB", @@ -11,19 +10,23 @@ "naming_series", "title", "status", - "patient", - "patient_name", - "patient_sex", - "patient_age", - "inpatient_record", + "appointment_type", + "appointment_for", "column_break_1", "company", "practitioner", "practitioner_name", "department", "service_unit", + "appointment_date", + "section_patient_details", + "patient", + "patient_name", + "inpatient_record", + "column_break_4pp7", + "patient_sex", + "patient_age", "section_break_12", - "appointment_type", "duration", "procedure_template", "get_procedure_from_encounter", @@ -32,11 +35,10 @@ "therapy_type", "get_prescribed_therapies", "column_break_17", - "appointment_date", "appointment_time", "appointment_datetime", - "event", "add_video_conferencing", + "event", "google_meet_link", "section_break_16", "mode_of_payment", @@ -47,6 +49,8 @@ "ref_sales_invoice", "section_break_3", "referring_practitioner", + "position_in_queue", + "appointment_based_on_check_in", "reminded", "column_break_36", "notes" @@ -78,6 +82,7 @@ "ignore_user_permissions": 1, "label": "Appointment Type", "options": "Appointment Type", + "reqd": 1, "set_only_once": 1 }, { @@ -100,12 +105,12 @@ "in_filter": 1, "in_list_view": 1, "label": "Status", - "options": "\nScheduled\nOpen\nClosed\nCancelled", + "options": "\nScheduled\nOpen\nChecked In\nChecked Out\nClosed\nCancelled\nNo Show", "read_only": 1, "search_index": 1 }, { - "depends_on": "eval:doc.patient;", + "depends_on": "eval:doc.patient", "fieldname": "procedure_template", "fieldtype": "Link", "label": "Clinical Procedure Template", @@ -132,34 +137,39 @@ "fieldname": "service_unit", "fieldtype": "Link", "label": "Service Unit", + "mandatory_depends_on": "eval:doc.appointment_for==\"Service Unit\"", "options": "Healthcare Service Unit", - "read_only": 1 + "read_only": 1, + "read_only_depends_on": "eval:doc.appointment_for!=\"Service Unit\"" }, { - "depends_on": "eval:doc.practitioner;", + "depends_on": "eval:!doc.__is_local", "fieldname": "section_break_12", "fieldtype": "Section Break", "label": "Appointment Details" }, { + "depends_on": "eval:doc.appointment_for==\"Practitioner\"", "fieldname": "practitioner", "fieldtype": "Link", "in_standard_filter": 1, "label": "Healthcare Practitioner", "options": "Healthcare Practitioner", - "reqd": 1, "search_index": 1, "set_only_once": 1 }, { "fetch_from": "practitioner.department", + "fetch_if_empty": 1, "fieldname": "department", "fieldtype": "Link", "ignore_user_permissions": 1, "in_list_view": 1, "in_standard_filter": 1, "label": "Department", + "mandatory_depends_on": "eval:doc.appointment_for==\"Department\"", "options": "Medical Department", + "read_only_depends_on": "eval:doc.appointment_for!=\"Department\"", "search_index": 1, "set_only_once": 1 }, @@ -174,16 +184,18 @@ "in_standard_filter": 1, "label": "Date", "read_only": 1, + "read_only_depends_on": "eval:doc.appointment_for==\"Practitioner\"", "reqd": 1, "search_index": 1 }, { + "depends_on": "eval: doc.appointment_for == \"Practitioner\"", "fieldname": "appointment_time", "fieldtype": "Time", "in_list_view": 1, "label": "Time", - "read_only": 1, - "reqd": 1 + "mandatory_depends_on": "eval:doc.appointment_based_on_check_in == false && doc.appointment_for == \"Practitioner\";", + "read_only": 1 }, { "fieldname": "section_break_16", @@ -357,6 +369,7 @@ }, { "default": "0", + "depends_on": "eval: doc.appointment_for == \"Practitioner\"", "fieldname": "add_video_conferencing", "fieldtype": "Check", "label": "Add Video Conferencing" @@ -367,10 +380,43 @@ "label": "Event", "options": "Event", "read_only": 1 + }, + { + "default": "0", + "fieldname": "appointment_based_on_check_in", + "fieldtype": "Check", + "label": "Appointment Based On Check In", + "read_only": 1 + }, + { + "fieldname": "position_in_queue", + "fieldtype": "Int", + "label": "Position In Queue", + "non_negative": 1, + "read_only": 1 + }, + { + "fetch_from": "appointment_type.allow_booking_for", + "fieldname": "appointment_for", + "fieldtype": "Select", + "label": "Appointment For", + "options": "\nPractitioner\nDepartment\nService Unit", + "read_only": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "section_patient_details", + "fieldtype": "Section Break", + "label": "Patient Details" + }, + { + "fieldname": "column_break_4pp7", + "fieldtype": "Column Break" } ], "links": [], - "modified": "2022-08-16 19:10:10.528001", + "modified": "2023-06-10 13:39:03.455568", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Appointment", diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py index 3cc65a1031..bd874cc99d 100755 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py @@ -23,7 +23,7 @@ get_income_account, get_receivable_account, ) -from healthcare.healthcare.utils import get_service_item_and_practitioner_charge +from healthcare.healthcare.utils import get_appointment_billing_item_and_rate class MaximumCapacityError(frappe.ValidationError): @@ -37,12 +37,14 @@ class OverlapError(frappe.ValidationError): class PatientAppointment(Document): def validate(self): self.validate_overlaps() + self.validate_based_on_appointments_for() self.validate_service_unit() self.set_appointment_datetime() self.validate_customer_created() self.set_status() self.set_title() self.update_event() + self.set_postition_in_queue() def on_update(self): invoice_appointment(self) @@ -55,9 +57,14 @@ def after_insert(self): self.insert_calendar_event() def set_title(self): - self.title = _("{0} with {1}").format( - self.patient_name or self.patient, self.practitioner_name or self.practitioner - ) + if self.practitioner: + self.title = _("{0} with {1}").format( + self.patient_name or self.patient, self.practitioner_name or self.practitioner + ) + else: + self.title = _("{0} at {1}").format( + self.patient_name or self.patient, self.get(frappe.scrub(self.appointment_for)) + ) def set_status(self): today = getdate() @@ -65,11 +72,34 @@ def set_status(self): # If appointment is created for today set status as Open else Scheduled if appointment_date == today: - self.status = "Open" + if self.status not in ["Checked In", "Checked Out"]: + self.status = "Open" + elif appointment_date > today: self.status = "Scheduled" + elif appointment_date < today: + if self.status == "Scheduled": + self.status = "No Show" + def validate_overlaps(self): + if self.appointment_based_on_check_in: + if frappe.db.exists( + { + "doctype": "Patient Appointment", + "patient": self.patient, + "appointment_date": self.appointment_date, + "appointment_time": self.appointment_time, + "appointment_based_on_check_in": True, + "name": ["!=", self.name], + } + ): + frappe.throw(_("Patient already has an appointment booked for the same day!"), OverlapError) + return + + if not self.practitioner: + return + end_time = datetime.datetime.combine( getdate(self.appointment_date), get_time(self.appointment_time) ) + datetime.timedelta(minutes=flt(self.duration)) @@ -136,6 +166,45 @@ def validate_overlaps(self): OverlapError, ) + def validate_based_on_appointments_for(self): + if self.appointment_for: + # fieldname: practitioner / department / service_unit + appointment_for_field = frappe.scrub(self.appointment_for) + + # validate if respective field is set + if not self.get(appointment_for_field): + frappe.throw( + _("Please enter {}").format(frappe.bold(self.appointment_for)), + frappe.MandatoryError, + ) + + if self.appointment_for == "Practitioner": + # appointments for practitioner are validated separately, + # based on practitioner schedule + return + + # validate if patient already has an appointment for the day + booked_appointment = frappe.db.exists( + "Patient Appointment", + { + "patient": self.patient, + "status": ["!=", "Cancelled"], + appointment_for_field: self.get(appointment_for_field), + "appointment_date": self.appointment_date, + "name": ["!=", self.name], + }, + ) + + if booked_appointment: + frappe.throw( + _("Patient already has an appointment {} booked for {} on {}").format( + get_link_to_form("Patient Appointment", booked_appointment), + frappe.bold(self.get(appointment_for_field)), + frappe.bold(format_date(self.appointment_date)), + ), + frappe.DuplicateEntryError, + ) + def validate_service_unit(self): if self.inpatient_record and self.service_unit: from healthcare.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import ( @@ -166,7 +235,7 @@ def set_appointment_datetime(self): def set_payment_details(self): if frappe.db.get_single_value("Healthcare Settings", "automate_appointment_invoicing"): - details = get_service_item_and_practitioner_charge(self) + details = get_appointment_billing_item_and_rate(self) self.db_set("billing_item", details.get("service_item")) if not self.paid_amount: self.db_set("paid_amount", details.get("practitioner_charge")) @@ -191,7 +260,10 @@ def update_prescription_details(self): frappe.db.set_value("Patient Appointment", self.name, "notes", comments) def update_fee_validity(self): - if not frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups"): + if ( + not frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") + or not self.practitioner + ): return fee_validity = manage_fee_validity(self) @@ -203,6 +275,9 @@ def update_fee_validity(self): ) def insert_calendar_event(self): + if not self.practitioner: + return + starts_on = datetime.datetime.combine( getdate(self.appointment_date), get_time(self.appointment_time) ) @@ -237,6 +312,7 @@ def insert_calendar_event(self): } ) participants = [] + participants.append( {"reference_doctype": "Healthcare Practitioner", "reference_docname": self.practitioner} ) @@ -287,6 +363,19 @@ def update_event(self): event_doc.reload() self.google_meet_link = event_doc.google_meet_link + def set_postition_in_queue(self): + if self.status == "Checked In" and not self.position_in_queue: + app_count = frappe.db.count( + "Patient Appointment", + { + "status": "Checked In", + "practitioner": self.practitioner, + "service_unit": self.service_unit, + "appointment_time": self.appointment_time, + }, + ) + self.position_in_queue = app_count + 1 + @frappe.whitelist() def check_payment_fields_reqd(patient): @@ -370,7 +459,7 @@ def check_is_new_patient(patient, name=None): def get_appointment_item(appointment_doc, item): - details = get_service_item_and_practitioner_charge(appointment_doc) + details = get_appointment_billing_item_and_rate(appointment_doc) charge = appointment_doc.paid_amount or details.get("practitioner_charge") item.item_code = details.get("service_item") item.description = _("Consulting Charges: {0}").format(appointment_doc.practitioner) @@ -563,7 +652,7 @@ def get_available_slots(practitioner_doc, date): appointments = frappe.get_all( "Patient Appointment", filters=filters, - fields=["name", "appointment_time", "duration", "status"], + fields=["name", "appointment_time", "duration", "status", "appointment_date"], ) slot_details.append( @@ -577,7 +666,6 @@ def get_available_slots(practitioner_doc, date): "tele_conf": practitioner_schedule.allow_video_conferencing, } ) - return slot_details diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment_list.js b/healthcare/healthcare/doctype/patient_appointment/patient_appointment_list.js index 721887b459..0ae87f2060 100644 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment_list.js +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment_list.js @@ -9,7 +9,9 @@ frappe.listview_settings['Patient Appointment'] = { "Scheduled": "yellow", "Closed": "green", "Cancelled": "red", - "Expired": "grey" + "Expired": "grey", + "Checked In": "blue", + "Checked Out": "orange" }; return [__(doc.status), colors[doc.status], "status,=," + doc.status]; } diff --git a/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py index a8d8db0ea9..d2055d55d8 100644 --- a/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -83,12 +83,22 @@ def test_auto_invoicing(self): 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_practitioner_department(self): patient, practitioner = create_healthcare_docs() + frappe.db.set_value( + "Healthcare Practitioner", + practitioner, + { + "op_consulting_charge": 0, + "inpatient_visit_charge": 0, + }, + ) medical_department = create_medical_department() frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) - appointment_type = create_appointment_type({"medical_department": medical_department}) + appointment_type = create_appointment_type( + {"medical_department": medical_department, "op_consulting_charge": 200} + ) appointment = create_appointment( patient, @@ -112,8 +122,98 @@ def test_auto_invoicing_based_on_department(self): frappe.db.get_value("Sales Invoice", sales_invoice_name, "paid_amount"), appointment.paid_amount ) + def test_auto_invoicing_based_on_department(self): + frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 1) + frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + item = create_healthcare_service_items() + department_name = create_medical_department(id=111) # "_Test Medical Department 111" + items = [ + { + "dt": "Medical Department", + "dn": department_name, + "op_consulting_charge_item": item, + "op_consulting_charge": 1000, + } + ] + appointment_type = create_appointment_type( + args={ + "name": "_Test General OP", + "allow_booking_for": "Department", + "items": items, + "duration": 15, + } + ) + appointment = frappe.new_doc("Patient Appointment") + appointment.patient = create_patient() + appointment.appointment_type = appointment_type.name + appointment.department = department_name + appointment.appointment_date = add_days(nowdate(), 2) + appointment.company = "_Test Company" + + appointment.save(ignore_permissions=True) + + self.assertEqual(appointment.invoiced, 1) + self.assertEqual(appointment.billing_item, item) + self.assertEqual(appointment.paid_amount, 1000) + + sales_invoice_name = frappe.db.get_value( + "Sales Invoice Item", {"reference_dn": appointment.name}, "parent" + ) + self.assertTrue(sales_invoice_name) + + def test_auto_invoicing_based_on_service_unit(self): + frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) + frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + item = create_healthcare_service_items() + service_unit_type = create_service_unit_type(id=11, allow_appointments=1) + service_unit = create_service_unit( + id=101, + service_unit_type=service_unit_type, + ) + items = [ + { + "dt": "Healthcare Service Unit", + "dn": service_unit, + "op_consulting_charge_item": item, + "op_consulting_charge": 2000, + } + ] + appointment_type = create_appointment_type( + args={ + "name": "_Test XRay Modality", + "allow_booking_for": "Service Unit", + "items": items, + "duration": 15, + } + ) + appointment = frappe.new_doc("Patient Appointment") + appointment.patient = create_patient() + appointment.appointment_type = appointment_type.name + appointment.service_unit = service_unit + appointment.appointment_date = add_days(nowdate(), 3) + appointment.company = "_Test Company" + + appointment.save(ignore_permissions=True) + + self.assertEqual(appointment.invoiced, 1) + self.assertEqual(appointment.billing_item, item) + self.assertEqual(appointment.paid_amount, 2000) + + sales_invoice_name = frappe.db.get_value( + "Sales Invoice Item", {"reference_dn": appointment.name}, "parent" + ) + self.assertTrue(sales_invoice_name) + def test_auto_invoicing_according_to_appointment_type_charge(self): patient, practitioner = create_healthcare_docs() + frappe.db.set_value( + "Healthcare Practitioner", + practitioner, + { + "op_consulting_charge": 0, + "inpatient_visit_charge": 0, + }, + ) frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) @@ -178,8 +278,8 @@ def test_appointment_booking_for_admission_service_unit(self): ) frappe.db.sql("""delete from `tabInpatient Record`""") - patient, practitioner = create_healthcare_docs() patient = create_patient() + practitioner = create_practitioner() # Schedule Admission ip_record = create_inpatient(patient) ip_record.expected_length_of_stay = 0 @@ -364,6 +464,51 @@ def test_teleconsultation(self): test_appointment_reschedule(self, appointment) test_appointment_cancel(self, appointment) + def test_appointment_based_on_check_in(self): + from healthcare.healthcare.doctype.patient_appointment.patient_appointment import OverlapError + + patient, practitioner = create_healthcare_docs(id=1) + patient_1, practitioner_1 = create_healthcare_docs(id=2) + + create_appointment( + patient, + practitioner, + nowdate(), + appointment_based_on_check_in=True, + appointment_time="09:00", + ) + appointment_1 = create_appointment( + patient, + practitioner, + nowdate(), + save=0, + appointment_based_on_check_in=True, + appointment_time="09:00", + ) + # same patient cannot have multiple appointments for same practitioner + self.assertRaises(OverlapError, appointment_1.save) + + appointment_1 = create_appointment( + patient, + practitioner_1, + nowdate(), + save=0, + appointment_based_on_check_in=True, + appointment_time="09:00", + ) + # same patient cannot have multiple appointments for different practitioners + self.assertRaises(OverlapError, appointment_1.save) + + appointment_2 = create_appointment( + patient_1, + practitioner, + nowdate(), + appointment_based_on_check_in=True, + appointment_time="09:00", + ) + # different pracititoner can have multiple same time and date appointments for different patients + self.assertTrue(appointment_2.name) + def create_healthcare_docs(id=0): patient = create_patient(id) @@ -398,7 +543,6 @@ def create_medical_department(id=0): 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 @@ -447,6 +591,8 @@ def create_appointment( appointment_type=None, save=1, department=None, + appointment_based_on_check_in=None, + appointment_time=None, ): item = create_healthcare_service_items() frappe.db.set_single_value("Healthcare Settings", "inpatient_visit_charge_item", item) @@ -454,19 +600,22 @@ def create_appointment( appointment = frappe.new_doc("Patient Appointment") appointment.patient = patient appointment.practitioner = practitioner - appointment.department = department or "_Test Medical Department" + appointment.department = department or create_medical_department() appointment.appointment_date = appointment_date appointment.company = "_Test Company" appointment.duration = 15 + appointment.appointment_type = appointment_type or create_appointment_type().name if service_unit: appointment.service_unit = service_unit if invoice: appointment.mode_of_payment = "Cash" - if appointment_type: - appointment.appointment_type = appointment_type if procedure_template: appointment.procedure_template = create_clinical_procedure_template().get("name") + if appointment_based_on_check_in: + appointment.appointment_based_on_check_in = True + if appointment_time: + appointment.appointment_time = appointment_time if save: appointment.save(ignore_permissions=True) @@ -508,7 +657,7 @@ def create_appointment_type(args=None): if not args: args = frappe.local.form_dict - name = args.get("name") or "Test Appointment Type wise Charge" + name = args.get("name", "_Test Appointment Type") if frappe.db.exists("Appointment Type", name): return frappe.get_doc("Appointment Type", name) @@ -519,15 +668,16 @@ def create_appointment_type(args=None): { "medical_department": args.get("medical_department") or "_Test Medical Department", "op_consulting_charge_item": item, - "op_consulting_charge": 200, + "op_consulting_charge": args.get("op_consulting_charge", 200), } ] return frappe.get_doc( { "doctype": "Appointment Type", - "appointment_type": args.get("name") or "Test Appointment Type wise Charge", - "default_duration": args.get("default_duration") or 20, - "color": args.get("color") or "#7575ff", + "appointment_type": name, + "allow_booking_for": args.get("allow_booking_for", "Practitioner"), + "default_duration": args.get("default_duration", 20), + "color": args.get("color", "#7575ff"), "price_list": args.get("price_list") or frappe.db.get_value("Price List", {"selling": 1}), "items": args.get("items") or items, } diff --git a/healthcare/healthcare/doctype/practitioner_schedule/practitioner_schedule.js b/healthcare/healthcare/doctype/practitioner_schedule/practitioner_schedule.js index a9db5f3ea6..84a3115b17 100644 --- a/healthcare/healthcare/doctype/practitioner_schedule/practitioner_schedule.js +++ b/healthcare/healthcare/doctype/practitioner_schedule/practitioner_schedule.js @@ -7,7 +7,8 @@ frappe.ui.form.on('Practitioner Schedule', { cur_frm.fields_dict["time_slots"].grid.add_custom_button(__('Add Time Slots'), () => { let d = new frappe.ui.Dialog({ fields: [ - {fieldname: 'days', label: __('Select Days'), fieldtype: 'MultiSelect', + { + fieldname: 'days', label: __('Select Days'), fieldtype: 'MultiSelect', options:[ {value:'Sunday', label:__('Sunday')}, {value:'Monday', label:__('Monday')}, @@ -16,15 +17,31 @@ frappe.ui.form.on('Practitioner Schedule', { {value:'Thursday', label:__('Thursday')}, {value:'Friday', label:__('Friday')}, {value:'Saturday', label:__('Saturday')}, - ], reqd: 1}, - {fieldname: 'from_time', label: __('From'), fieldtype: 'Time', - 'default': '09:00:00', reqd: 1}, - {fieldname: 'to_time', label: __('To'), fieldtype: 'Time', - 'default': '12:00:00', reqd: 1}, - {fieldname: 'duration', label: __('Appointment Duration (mins)'), - fieldtype:'Int', 'default': 15, reqd: 1}, + ], reqd: 1 + }, + { + fieldname: 'from_time', label: __('From'), fieldtype: 'Time', + 'default': '09:00:00', reqd: 1 + }, + { + fieldname: 'to_time', label: __('To'), fieldtype: 'Time', + 'default': '12:00:00', reqd: 1 + }, + { + fieldname: 'duration', label: __('Appointment Duration (mins)'), + fieldtype:'Int', 'default': 15, + }, + { + fieldname: 'create_slots', label: __('Create Slots'), fieldtype: 'Check', + 'default': 1 + }, + { + fieldname: 'maximum_appointments', label: __('Maximum Number of Appointments (Nos)'), + fieldtype:'Int', mandatory_depends_on: "eval:doc.create_slots==0", + depends_on: "eval:doc.create_slots==0" + }, ], - primary_action_label: __('Add Timeslots'), + primary_action_label: __('Add'), primary_action: () => { let values = d.get_values(); if (values) { @@ -39,41 +56,75 @@ frappe.ui.form.on('Practitioner Schedule', { function check_overlap_or_add_slot(week_day, cur_time, end_time, add_slots_to_child){ let overlap = false; - while (cur_time < end_time) { - let add_to_child = true; - let to_time = cur_time.clone().add(values.duration, 'minutes'); - if (to_time <= end_time) { - if (frm.doc.time_slots){ - frm.doc.time_slots.forEach(function(slot) { - if (slot.day == week_day){ - let slot_from_moment = moment(slot.from_time, 'HH:mm:ss'); - let slot_to_moment = moment(slot.to_time, 'HH:mm:ss'); - if (cur_time.isSame(slot_from_moment) || cur_time.isBetween(slot_from_moment, slot_to_moment) || - to_time.isSame(slot_to_moment) || to_time.isBetween(slot_from_moment, slot_to_moment)) { - overlap = true; - if (add_slots_to_child) { - frappe.show_alert({ - message:__('Time slot skiped, the slot {0} to {1} overlap exisiting slot {2} to {3}', - [cur_time.format('HH:mm:ss'), to_time.format('HH:mm:ss'), slot.from_time, slot.to_time]), - indicator:'orange' - }); - add_to_child = false; + if (values.create_slots) { + while (cur_time < end_time) { + let add_to_child = true; + let to_time = cur_time.clone().add(values.duration, 'minutes'); + if (to_time <= end_time) { + if (frm.doc.time_slots){ + frm.doc.time_slots.forEach(function(slot) { + if (slot.day == week_day){ + let slot_from_moment = moment(slot.from_time, 'HH:mm:ss'); + let slot_to_moment = moment(slot.to_time, 'HH:mm:ss'); + if (cur_time.isSame(slot_from_moment) || cur_time.isBetween(slot_from_moment, slot_to_moment) || + to_time.isSame(slot_to_moment) || to_time.isBetween(slot_from_moment, slot_to_moment)) { + overlap = true; + if (add_slots_to_child) { + frappe.show_alert({ + message:__('Time slot skiped, the slot {0} to {1} overlap exisiting slot {2} to {3}', + [cur_time.format('HH:mm:ss'), to_time.format('HH:mm:ss'), slot.from_time, slot.to_time]), + indicator:'orange' + }); + add_to_child = false; + } + } } + }); + } + // add a new timeslot + if (add_to_child && add_slots_to_child) { + frm.add_child('time_slots', { + from_time: cur_time.format('HH:mm:ss'), + to_time: to_time.format('HH:mm:ss'), + day: week_day + }); + slot_added = true; + } + } + cur_time = to_time; + } + } else { + // appointment for day + let add_to_child = true; + if (add_slots_to_child && frm.doc.time_slots){ + frm.doc.time_slots.forEach(function(slot) { + if (slot.day == week_day){ + let slot_from_moment = moment(slot.from_time, 'HH:mm:ss'); + let slot_to_moment = moment(slot.to_time, 'HH:mm:ss'); + if (cur_time.isSame(slot_from_moment) || cur_time.isBetween(slot_from_moment, slot_to_moment) || + end_time.isSame(slot_to_moment) || end_time.isBetween(slot_from_moment, slot_to_moment)) { + overlap = true; + if (add_slots_to_child) { + frappe.show_alert({ + message:__('Time slot skiped, the slot {0} to {1} overlap exisiting slot {2} to {3}', + [cur_time.format('HH:mm:ss'), end_time.format('HH:mm:ss'), slot.from_time, slot.to_time]), + indicator:'orange' + }); + add_to_child = false; } } - }); - } - // add a new timeslot - if (add_to_child && add_slots_to_child) { - frm.add_child('time_slots', { - from_time: cur_time.format('HH:mm:ss'), - to_time: to_time.format('HH:mm:ss'), - day: week_day - }); - slot_added = true; - } + } + }); + } + if (add_slots_to_child && add_to_child) { + frm.add_child('time_slots', { + from_time: cur_time.format('HH:mm:ss'), + to_time: end_time.format('HH:mm:ss'), + day: week_day, + maximum_appointments: values.maximum_appointments, + duration: values.duration, + }); } - cur_time = to_time; } return overlap; } @@ -111,7 +162,33 @@ frappe.ui.form.on('Practitioner Schedule', { } }, }); + d.fields_dict['create_slots'].df.onchange = () => { + set_maximum_no_of_appointments(d) + } + d.fields_dict['from_time'].df.onchange = () => { + set_maximum_no_of_appointments(d) + } + d.fields_dict['to_time'].df.onchange = () => { + set_maximum_no_of_appointments(d) + } + d.fields_dict['duration'].df.onchange = () => { + set_maximum_no_of_appointments(d) + } d.show(); }); } }); + +var set_maximum_no_of_appointments = function(d) { + if (!d.get_value("create_slots")) { + let interval = 0; + let max_apps = 0; + if (d.get_value("from_time") && d.get_value("to_time")) { + interval = (moment(d.get_value("from_time"), 'HH:mm:ss') - moment(d.get_value("to_time"), 'HH:mm:ss')) / 60000 | 0; + max_apps = Math.abs(interval) / d.get_value("duration") + d.set_value("maximum_appointments", Math.floor(max_apps)) + } + } else { + d.set_value("maximum_appointments", 0) + } +} \ No newline at end of file diff --git a/healthcare/healthcare/doctype/practitioner_schedule/practitioner_schedule.py b/healthcare/healthcare/doctype/practitioner_schedule/practitioner_schedule.py index b4f708021f..abaca75ac0 100644 --- a/healthcare/healthcare/doctype/practitioner_schedule/practitioner_schedule.py +++ b/healthcare/healthcare/doctype/practitioner_schedule/practitioner_schedule.py @@ -2,10 +2,25 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - +import frappe +from frappe import _ from frappe.model.document import Document +from frappe.utils import time_diff class PractitionerSchedule(Document): def autoname(self): self.name = self.schedule_name + + def validate(self): + if self.time_slots: + for slots in self.time_slots: + if slots.get("from_time") and slots.get("to_time") and slots.get("duration"): + time_diff_in_mins = ( + time_diff(slots.get("from_time"), slots.get("to_time")).total_seconds() / 60 + ) + maximum_apps = abs(time_diff_in_mins) / slots.get("duration") + if slots.get("maximum_appointments") > maximum_apps: + frappe.throw( + _(f"""Maximum appointments cannot be more than {maximum_apps} in row {slots.get("idx")}.""") + ) diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index 0d09171149..a42e81b34c 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -81,7 +81,7 @@ def get_appointments_to_invoice(patient, company): income_account = None service_item = None if appointment.practitioner: - details = get_service_item_and_practitioner_charge(appointment) + details = get_appointment_billing_item_and_rate(appointment) service_item = details.get("service_item") practitioner_charge = details.get("practitioner_charge") income_account = get_income_account(appointment.practitioner, appointment.company) @@ -119,7 +119,7 @@ def get_encounters_to_invoice(patient, company): ): continue - details = get_service_item_and_practitioner_charge(encounter) + details = get_appointment_billing_item_and_rate(encounter) service_item = details.get("service_item") practitioner_charge = details.get("practitioner_charge") income_account = get_income_account(encounter.practitioner, encounter.company) @@ -205,7 +205,6 @@ def get_clinical_procedures_to_invoice(patient, company): and procedure.status == "Completed" and not procedure.consumption_invoiced ): - service_item = frappe.db.get_single_value( "Healthcare Settings", "clinical_procedure_consumable_item" ) @@ -369,7 +368,7 @@ def get_therapy_sessions_to_invoice(patient, company): @frappe.whitelist() -def get_service_item_and_practitioner_charge(doc): +def get_appointment_billing_item_and_rate(doc): if isinstance(doc, str): doc = json.loads(doc) doc = frappe.get_doc(doc) @@ -377,34 +376,44 @@ def get_service_item_and_practitioner_charge(doc): service_item = None practitioner_charge = None department = doc.medical_department if doc.doctype == "Patient Encounter" else doc.department + service_unit = doc.service_unit if doc.doctype == "Patient Appointment" else None is_inpatient = doc.inpatient_record - if doc.get("appointment_type"): - service_item, practitioner_charge = get_appointment_type_service_item( - doc.appointment_type, department, is_inpatient + if doc.get("practitioner"): + service_item, practitioner_charge = get_practitioner_billing_details( + doc.practitioner, is_inpatient ) - if not service_item and not practitioner_charge: - service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient) - if not service_item: - service_item = get_healthcare_service_item(is_inpatient) + if not service_item and doc.get("appointment_type"): + service_item, appointment_charge = get_appointment_type_billing_details( + doc.appointment_type, department if department else service_unit, is_inpatient + ) + if not practitioner_charge: + practitioner_charge = appointment_charge + + if not service_item: + service_item = get_healthcare_service_item(is_inpatient) if not service_item: throw_config_service_item(is_inpatient) - if not practitioner_charge: + if not practitioner_charge and doc.get("practitioner"): throw_config_practitioner_charge(is_inpatient, doc.practitioner) + if not practitioner_charge and not doc.get("practitioner"): + throw_config_appointment_type_charge(is_inpatient, doc.appointment_type) + return {"service_item": service_item, "practitioner_charge": practitioner_charge} -def get_appointment_type_service_item(appointment_type, department, is_inpatient): - from healthcare.healthcare.doctype.appointment_type.appointment_type import ( - get_service_item_based_on_department, - ) +def get_appointment_type_billing_details(appointment_type, dep_su, is_inpatient): + from healthcare.healthcare.doctype.appointment_type.appointment_type import get_billing_details + + if not dep_su: + return None, None - item_list = get_service_item_based_on_department(appointment_type, department) + item_list = get_billing_details(appointment_type, dep_su) service_item = None practitioner_charge = None @@ -420,9 +429,9 @@ def get_appointment_type_service_item(appointment_type, department, is_inpatient def throw_config_service_item(is_inpatient): - service_item_label = _("Out Patient Consulting Charge Item") - if is_inpatient: - service_item_label = _("Inpatient Visit Charge Item") + service_item_label = ( + _("Inpatient Visit Charge Item") if is_inpatient else _("Out Patient Consulting Charge Item") + ) msg = _( ("Please Configure {0} in ").format(service_item_label) @@ -432,9 +441,7 @@ def throw_config_service_item(is_inpatient): def throw_config_practitioner_charge(is_inpatient, practitioner): - charge_name = _("OP Consulting Charge") - if is_inpatient: - charge_name = _("Inpatient Visit Charge") + charge_name = _("Inpatient Visit Charge") if is_inpatient else _("OP Consulting Charge") msg = _( ("Please Configure {0} for Healthcare Practitioner").format(charge_name) @@ -443,19 +450,28 @@ def throw_config_practitioner_charge(is_inpatient, practitioner): frappe.throw(msg, title=_("Missing Configuration")) -def get_practitioner_service_item(practitioner, is_inpatient): +def throw_config_appointment_type_charge(is_inpatient, appointment_type): + charge_name = _("Inpatient Visit Charge") if is_inpatient else _("OP Consulting Charge") + + msg = _( + ("Please Configure {0} for Appointment Type").format(charge_name) + + """ {0}""".format(appointment_type) + ) + frappe.throw(msg, title=_("Missing Configuration")) + + +def get_practitioner_billing_details(practitioner, is_inpatient): service_item = None practitioner_charge = None if is_inpatient: - service_item, practitioner_charge = frappe.db.get_value( - "Healthcare Practitioner", - practitioner, - ["inpatient_visit_charge_item", "inpatient_visit_charge"], - ) + fields = ["inpatient_visit_charge_item", "inpatient_visit_charge"] else: + fields = ["op_consulting_charge_item", "op_consulting_charge"] + + if practitioner: service_item, practitioner_charge = frappe.db.get_value( - "Healthcare Practitioner", practitioner, ["op_consulting_charge_item", "op_consulting_charge"] + "Healthcare Practitioner", practitioner, fields ) return service_item, practitioner_charge @@ -472,6 +488,7 @@ def get_healthcare_service_item(is_inpatient): return service_item +<<<<<<< HEAD def get_practitioner_charge(practitioner, is_inpatient): if is_inpatient: practitioner_charge = frappe.db.get_value( @@ -484,6 +501,13 @@ def get_practitioner_charge(practitioner, is_inpatient): if practitioner_charge: return practitioner_charge return False +======= +def manage_invoice_validate(doc, method): + if doc.service_unit and len(doc.items): + for item in doc.items: + if not item.service_unit: + item.service_unit = doc.service_unit +>>>>>>> 291095d (fix: book Patient Appointment based on check in) def manage_invoice_submit_cancel(doc, method): diff --git a/healthcare/patches.txt b/healthcare/patches.txt index 806e8cb203..04334f728c 100644 --- a/healthcare/patches.txt +++ b/healthcare/patches.txt @@ -1,3 +1,12 @@ +[pre_model_sync] healthcare.patches.v0_0.setup_abdm_custom_fields healthcare.patches.v0_0.set_medical_code_from_field_to_codification_table healthcare.patches.v15_0.set_fee_validity_status +<<<<<<< HEAD +======= +healthcare.patches.v15_0.create_custom_fields_in_sales_invoice_item + +[post_model_sync] +healthcare.patches.v15_0.rename_field_medical_department_in_appoitment_type_service_item +healthcare.patches.v15_0.set_default_dynamic_link_dt_for_appointment_type_service_item +>>>>>>> 291095d (fix: book Patient Appointment based on check in) diff --git a/healthcare/patches/v15_0/rename_field_medical_department_in_appoitment_type_service_item.py b/healthcare/patches/v15_0/rename_field_medical_department_in_appoitment_type_service_item.py new file mode 100644 index 0000000000..aaa8b6a845 --- /dev/null +++ b/healthcare/patches/v15_0/rename_field_medical_department_in_appoitment_type_service_item.py @@ -0,0 +1,5 @@ +from frappe.model.utils.rename_field import rename_field + + +def execute(): + rename_field("Appointment Type Service Item", "medical_department", "dn") diff --git a/healthcare/patches/v15_0/set_default_dynamic_link_dt_for_appointment_type_service_item.py b/healthcare/patches/v15_0/set_default_dynamic_link_dt_for_appointment_type_service_item.py new file mode 100644 index 0000000000..252ab74d04 --- /dev/null +++ b/healthcare/patches/v15_0/set_default_dynamic_link_dt_for_appointment_type_service_item.py @@ -0,0 +1,9 @@ +import frappe + + +def execute(): + docs = frappe.db.get_all("Appointment Type Service Item") + for doc in docs: + frappe.get_doc("Appointment Type Service Item", doc.name) + if doc.dn: + doc.db_set("dt", "Medical Department") diff --git a/healthcare/public/js/sales_invoice.js b/healthcare/public/js/sales_invoice.js index cb03cc9589..356ea1a98c 100644 --- a/healthcare/public/js/sales_invoice.js +++ b/healthcare/public/js/sales_invoice.js @@ -140,6 +140,7 @@ var make_list_row= function(columns, invoice_healthcare_services, result={}) { var set_primary_action= function(frm, dialog, $results, invoice_healthcare_services) { var me = this; dialog.set_primary_action(__('Add'), function() { + frm.clear_table('items'); let checked_values = get_checked_values($results); if(checked_values.length > 0){ if(invoice_healthcare_services) { From 75015090f867e6fdd03262c721a6e564fc10e6f6 Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Fri, 9 Jun 2023 14:04:58 +0530 Subject: [PATCH 19/39] fix: handle practitioner not set in appointment (cherry picked from commit 94a876c1093841f2cd8b4635e4907aa34484076e) --- healthcare/healthcare/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index a42e81b34c..10cbae3487 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -392,13 +392,16 @@ def get_appointment_billing_item_and_rate(doc): if not practitioner_charge: practitioner_charge = appointment_charge + if doc.get("practitioner") and not service_item and not practitioner_charge: + service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient) + if not service_item: service_item = get_healthcare_service_item(is_inpatient) if not service_item: throw_config_service_item(is_inpatient) - if not practitioner_charge and doc.get("practitioner"): + if doc.get("practitioner") and not practitioner_charge: throw_config_practitioner_charge(is_inpatient, doc.practitioner) if not practitioner_charge and not doc.get("practitioner"): From f49deb02beeeda40ace5d9b45f903567149593b5 Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Sat, 10 Jun 2023 15:37:32 +0530 Subject: [PATCH 20/39] feat: allow booking appointments against department / service_unit based on appointment type (cherry picked from commit fd2e2710924f659834ead91d5765e534b761318b) --- .../patient_appointment.js | 82 +++++++++++++++++++ .../patient_appointment.py | 15 ++-- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js index 4a99cefc11..ad72a1d10f 100644 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js @@ -196,6 +196,88 @@ frappe.ui.form.on('Patient Appointment', { }); }, + appointment_for: function(frm) { + if (frm.doc.appointment_for == 'Practitioner') { + if (!frm.doc.practitioner) { + frm.set_value('department', ''); + } + frm.set_value('service_unit', ''); + frm.trigger('set_check_availability_action'); + } else if (frm.doc.appointment_for == 'Service Unit') { + frm.set_value({ + 'practitioner': '', + 'practitioner_name': '', + 'department': '', + }); + frm.trigger('set_book_action'); + } else if (frm.doc.appointment_for == 'Department') { + frm.set_value({ + 'practitioner': '', + 'practitioner_name': '', + 'service_unit': '', + }); + frm.trigger('set_book_action'); + } else { + if (frm.doc.appointment_for == 'Department') { + frm.set_value('service_unit', ''); + } + frm.set_value({ + 'practitioner': '', + 'practitioner_name': '', + 'department': '', + 'service_unit': '', + }); + frm.page.clear_primary_action(); + } + }, + + set_book_action: function(frm) { + frm.page.set_primary_action(__('Book'), function() { + frm.enable_save(); + frm.save(); + }); + }, + + set_check_availability_action: function(frm) { + frm.page.set_primary_action(__('Check Availability'), function() { + if (!frm.doc.patient) { + frappe.msgprint({ + title: __('Not Allowed'), + message: __('Please select Patient first'), + indicator: 'red' + }); + } else { + frappe.call({ + method: 'healthcare.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd', + args: { 'patient': frm.doc.patient }, + callback: function(data) { + if (data.message == true) { + if (frm.doc.mode_of_payment && frm.doc.paid_amount) { + check_and_set_availability(frm); + } + if (!frm.doc.mode_of_payment) { + frappe.msgprint({ + title: __('Not Allowed'), + message: __('Please select a Mode of Payment first'), + indicator: 'red' + }); + } + if (!frm.doc.paid_amount) { + frappe.msgprint({ + title: __('Not Allowed'), + message: __('Please set the Paid Amount first'), + indicator: 'red' + }); + } + } else { + check_and_set_availability(frm); + } + } + }); + } + }); + }, + patient: function(frm) { if (frm.doc.patient) { frm.trigger('toggle_payment_fields'); diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py index bd874cc99d..94d646dcda 100755 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py @@ -37,7 +37,7 @@ class OverlapError(frappe.ValidationError): class PatientAppointment(Document): def validate(self): self.validate_overlaps() - self.validate_based_on_appointments_for() + self.validate_appointments_without_practitioner() self.validate_service_unit() self.set_appointment_datetime() self.validate_customer_created() @@ -166,8 +166,8 @@ def validate_overlaps(self): OverlapError, ) - def validate_based_on_appointments_for(self): - if self.appointment_for: + def validate_appointments_without_practitioner(self): + if not self.practitioner and self.appointment_type: # fieldname: practitioner / department / service_unit appointment_for_field = frappe.scrub(self.appointment_for) @@ -178,12 +178,7 @@ def validate_based_on_appointments_for(self): frappe.MandatoryError, ) - if self.appointment_for == "Practitioner": - # appointments for practitioner are validated separately, - # based on practitioner schedule - return - - # validate if patient already has an appointment for the day + # validate if patient already has an appointment booked_appointment = frappe.db.exists( "Patient Appointment", { @@ -197,7 +192,7 @@ def validate_based_on_appointments_for(self): if booked_appointment: frappe.throw( - _("Patient already has an appointment {} booked for {} on {}").format( + _("Patient already has an appointment {} booked at {} on {}").format( get_link_to_form("Patient Appointment", booked_appointment), frappe.bold(self.get(appointment_for_field)), frappe.bold(format_date(self.appointment_date)), From b6347cde0b393d69ca62935ee8ea1b715cdb3b1e Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Sat, 10 Jun 2023 20:21:05 +0530 Subject: [PATCH 21/39] fix: prioritize charges configured in practitioner doc above appointment type and settings (cherry picked from commit 92c141cf327e51dd8e5b1f28abec40366c9fce2f) --- healthcare/healthcare/utils.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index 10cbae3487..5cb9e3700f 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -381,20 +381,15 @@ def get_appointment_billing_item_and_rate(doc): is_inpatient = doc.inpatient_record if doc.get("practitioner"): - service_item, practitioner_charge = get_practitioner_billing_details( - doc.practitioner, is_inpatient - ) + service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient) - if not service_item and doc.get("appointment_type"): - service_item, appointment_charge = get_appointment_type_billing_details( - doc.appointment_type, department if department else service_unit, is_inpatient + if doc.get("appointment_type") and not service_item: + service_item, appointment_charge = get_appointment_type_service_item( + doc.appointment_type, department, is_inpatient ) if not practitioner_charge: practitioner_charge = appointment_charge - if doc.get("practitioner") and not service_item and not practitioner_charge: - service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient) - if not service_item: service_item = get_healthcare_service_item(is_inpatient) From 1bcb28dc1ee5b789ab773fd84e74dfbde0251997 Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Sun, 11 Jun 2023 15:19:50 +0530 Subject: [PATCH 22/39] feat: option to set charges for service unit / department in appointment_type changes to get dpartemnt / service unit charges patches to rename field and set default medical department (cherry picked from commit 963ba8a930fcd6939122ba8e33cefabe6f7afd55) --- .../patient_appointment/patient_appointment.py | 15 ++++++++++----- healthcare/healthcare/utils.py | 12 +++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py index 94d646dcda..bd874cc99d 100755 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py @@ -37,7 +37,7 @@ class OverlapError(frappe.ValidationError): class PatientAppointment(Document): def validate(self): self.validate_overlaps() - self.validate_appointments_without_practitioner() + self.validate_based_on_appointments_for() self.validate_service_unit() self.set_appointment_datetime() self.validate_customer_created() @@ -166,8 +166,8 @@ def validate_overlaps(self): OverlapError, ) - def validate_appointments_without_practitioner(self): - if not self.practitioner and self.appointment_type: + def validate_based_on_appointments_for(self): + if self.appointment_for: # fieldname: practitioner / department / service_unit appointment_for_field = frappe.scrub(self.appointment_for) @@ -178,7 +178,12 @@ def validate_appointments_without_practitioner(self): frappe.MandatoryError, ) - # validate if patient already has an appointment + if self.appointment_for == "Practitioner": + # appointments for practitioner are validated separately, + # based on practitioner schedule + return + + # validate if patient already has an appointment for the day booked_appointment = frappe.db.exists( "Patient Appointment", { @@ -192,7 +197,7 @@ def validate_appointments_without_practitioner(self): if booked_appointment: frappe.throw( - _("Patient already has an appointment {} booked at {} on {}").format( + _("Patient already has an appointment {} booked for {} on {}").format( get_link_to_form("Patient Appointment", booked_appointment), frappe.bold(self.get(appointment_for_field)), frappe.bold(format_date(self.appointment_date)), diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index 5cb9e3700f..a42e81b34c 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -381,11 +381,13 @@ def get_appointment_billing_item_and_rate(doc): is_inpatient = doc.inpatient_record if doc.get("practitioner"): - service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient) + service_item, practitioner_charge = get_practitioner_billing_details( + doc.practitioner, is_inpatient + ) - if doc.get("appointment_type") and not service_item: - service_item, appointment_charge = get_appointment_type_service_item( - doc.appointment_type, department, is_inpatient + if not service_item and doc.get("appointment_type"): + service_item, appointment_charge = get_appointment_type_billing_details( + doc.appointment_type, department if department else service_unit, is_inpatient ) if not practitioner_charge: practitioner_charge = appointment_charge @@ -396,7 +398,7 @@ def get_appointment_billing_item_and_rate(doc): if not service_item: throw_config_service_item(is_inpatient) - if doc.get("practitioner") and not practitioner_charge: + if not practitioner_charge and doc.get("practitioner"): throw_config_practitioner_charge(is_inpatient, doc.practitioner) if not practitioner_charge and not doc.get("practitioner"): From 48e1a59ce8e8dc77144e0ee46a1471d4c07656e0 Mon Sep 17 00:00:00 2001 From: Akash Date: Mon, 24 Jul 2023 17:44:52 +0530 Subject: [PATCH 23/39] fix: add nosemgrep in create_appointment_type method (cherry picked from commit 7d80e377a9ab8322adc3aba23a03741f4746b93f) --- .../patient_appointment/test_patient_appointment.py | 2 +- .../doctype/practitioner_schedule/practitioner_schedule.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py index d2055d55d8..51256bcb27 100644 --- a/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -653,7 +653,7 @@ def create_clinical_procedure_template(): return template -def create_appointment_type(args=None): +def create_appointment_type(args=None): # nosemgrep if not args: args = frappe.local.form_dict diff --git a/healthcare/healthcare/doctype/practitioner_schedule/practitioner_schedule.py b/healthcare/healthcare/doctype/practitioner_schedule/practitioner_schedule.py index abaca75ac0..0555b107d3 100644 --- a/healthcare/healthcare/doctype/practitioner_schedule/practitioner_schedule.py +++ b/healthcare/healthcare/doctype/practitioner_schedule/practitioner_schedule.py @@ -19,8 +19,9 @@ def validate(self): time_diff_in_mins = ( time_diff(slots.get("from_time"), slots.get("to_time")).total_seconds() / 60 ) - maximum_apps = abs(time_diff_in_mins) / slots.get("duration") + maximum_apps = int(abs(time_diff_in_mins) / slots.get("duration")) if slots.get("maximum_appointments") > maximum_apps: - frappe.throw( - _(f"""Maximum appointments cannot be more than {maximum_apps} in row {slots.get("idx")}.""") + msg = _("Maximum appointments cannot be more than {0} in row #{1}").format( + maximum_apps, slots.get("idx") ) + frappe.throw(msg) From 48afc0658ebc20a0c7e3560fc05d7a8f0e2ba0a8 Mon Sep 17 00:00:00 2001 From: Akash Date: Tue, 25 Jul 2023 13:41:57 +0530 Subject: [PATCH 24/39] fix(patch): Set Allow Booking For in Appointment Type (cherry picked from commit 025117b6901d24f60bae140c1cc08937aedf5766) # Conflicts: # healthcare/patches.txt --- healthcare/patches.txt | 4 ++++ .../set_allow_booking_for_in_appointment_type.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 healthcare/patches/v15_0/set_allow_booking_for_in_appointment_type.py diff --git a/healthcare/patches.txt b/healthcare/patches.txt index 04334f728c..44745407e3 100644 --- a/healthcare/patches.txt +++ b/healthcare/patches.txt @@ -9,4 +9,8 @@ healthcare.patches.v15_0.create_custom_fields_in_sales_invoice_item [post_model_sync] healthcare.patches.v15_0.rename_field_medical_department_in_appoitment_type_service_item healthcare.patches.v15_0.set_default_dynamic_link_dt_for_appointment_type_service_item +<<<<<<< HEAD >>>>>>> 291095d (fix: book Patient Appointment based on check in) +======= +healthcare.patches.v15_0.set_allow_booking_for_in_appointment_type +>>>>>>> 025117b (fix(patch): Set Allow Booking For in Appointment Type) diff --git a/healthcare/patches/v15_0/set_allow_booking_for_in_appointment_type.py b/healthcare/patches/v15_0/set_allow_booking_for_in_appointment_type.py new file mode 100644 index 0000000000..a97d0631e6 --- /dev/null +++ b/healthcare/patches/v15_0/set_allow_booking_for_in_appointment_type.py @@ -0,0 +1,15 @@ +import frappe + + +def execute(): + appointment_types = frappe.db.get_all("Appointment Type") + for at in appointment_types: + frappe.db.set_value( + "Appointment Type", at.name, "allow_booking_for", "Practitioner" + ) + + appointment_type_items = frappe.db.get_all("Appointment Type Service Item") + for ati in appointment_type_items: + frappe.db.set_value( + "Appointment Type Service Item", ati.name, "dt", "Medical Department" + ) From eb91d62d54773bb5bb0cebcaa551a1e9a9066e81 Mon Sep 17 00:00:00 2001 From: Akash Date: Tue, 25 Jul 2023 14:25:58 +0530 Subject: [PATCH 25/39] fix: Linters Issue (cherry picked from commit a10339094dd8c1707086906b5ab8dd56b02ee533) --- healthcare/healthcare/utils.py | 15 --------------- healthcare/patches.txt | 9 +-------- .../set_allow_booking_for_in_appointment_type.py | 8 ++------ 3 files changed, 3 insertions(+), 29 deletions(-) diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index a42e81b34c..eb3bdb5dd5 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -488,26 +488,11 @@ def get_healthcare_service_item(is_inpatient): return service_item -<<<<<<< HEAD -def get_practitioner_charge(practitioner, is_inpatient): - if is_inpatient: - practitioner_charge = frappe.db.get_value( - "Healthcare Practitioner", practitioner, "inpatient_visit_charge" - ) - else: - practitioner_charge = frappe.db.get_value( - "Healthcare Practitioner", practitioner, "op_consulting_charge" - ) - if practitioner_charge: - return practitioner_charge - return False -======= def manage_invoice_validate(doc, method): if doc.service_unit and len(doc.items): for item in doc.items: if not item.service_unit: item.service_unit = doc.service_unit ->>>>>>> 291095d (fix: book Patient Appointment based on check in) def manage_invoice_submit_cancel(doc, method): diff --git a/healthcare/patches.txt b/healthcare/patches.txt index 44745407e3..d26522aa83 100644 --- a/healthcare/patches.txt +++ b/healthcare/patches.txt @@ -2,15 +2,8 @@ healthcare.patches.v0_0.setup_abdm_custom_fields healthcare.patches.v0_0.set_medical_code_from_field_to_codification_table healthcare.patches.v15_0.set_fee_validity_status -<<<<<<< HEAD -======= -healthcare.patches.v15_0.create_custom_fields_in_sales_invoice_item [post_model_sync] healthcare.patches.v15_0.rename_field_medical_department_in_appoitment_type_service_item healthcare.patches.v15_0.set_default_dynamic_link_dt_for_appointment_type_service_item -<<<<<<< HEAD ->>>>>>> 291095d (fix: book Patient Appointment based on check in) -======= -healthcare.patches.v15_0.set_allow_booking_for_in_appointment_type ->>>>>>> 025117b (fix(patch): Set Allow Booking For in Appointment Type) +healthcare.patches.v15_0.set_allow_booking_for_in_appointment_type \ No newline at end of file diff --git a/healthcare/patches/v15_0/set_allow_booking_for_in_appointment_type.py b/healthcare/patches/v15_0/set_allow_booking_for_in_appointment_type.py index a97d0631e6..d2ebd828c8 100644 --- a/healthcare/patches/v15_0/set_allow_booking_for_in_appointment_type.py +++ b/healthcare/patches/v15_0/set_allow_booking_for_in_appointment_type.py @@ -4,12 +4,8 @@ def execute(): appointment_types = frappe.db.get_all("Appointment Type") for at in appointment_types: - frappe.db.set_value( - "Appointment Type", at.name, "allow_booking_for", "Practitioner" - ) + frappe.db.set_value("Appointment Type", at.name, "allow_booking_for", "Practitioner") appointment_type_items = frappe.db.get_all("Appointment Type Service Item") for ati in appointment_type_items: - frappe.db.set_value( - "Appointment Type Service Item", ati.name, "dt", "Medical Department" - ) + frappe.db.set_value("Appointment Type Service Item", ati.name, "dt", "Medical Department") From b1abff53d1108990fd329ca61c86cdd621ab9470 Mon Sep 17 00:00:00 2001 From: Akash Date: Sun, 17 Sep 2023 12:10:16 +0530 Subject: [PATCH 26/39] fix: remove unused method from utils --- healthcare/healthcare/utils.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index eb3bdb5dd5..f8fad5b730 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -488,13 +488,6 @@ def get_healthcare_service_item(is_inpatient): return service_item -def manage_invoice_validate(doc, method): - if doc.service_unit and len(doc.items): - for item in doc.items: - if not item.service_unit: - item.service_unit = doc.service_unit - - def manage_invoice_submit_cancel(doc, method): if not doc.patient: return From 256344ad0bb4a15171ffb69e6d3ce41ff72c14ab Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Wed, 21 Jun 2023 16:49:09 +0530 Subject: [PATCH 27/39] fix: add Service Unit, Practitioner and Department custom field in Sales Invoice Item --- .../custom_doctype/sales_invoice.py | 5 +++ .../healthcare/doctype/lab_test/lab_test.json | 9 +++- .../healthcare/doctype/lab_test/lab_test.py | 9 ++-- healthcare/healthcare/utils.py | 7 +++ healthcare/hooks.py | 1 + healthcare/patches.txt | 3 +- ...ate_custom_fields_in_sales_invoice_item.py | 43 +++++++++++++++++++ healthcare/public/js/sales_invoice.js | 18 ++++++++ healthcare/setup.py | 31 +++++++++++++ 9 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 healthcare/patches/v15_0/create_custom_fields_in_sales_invoice_item.py diff --git a/healthcare/healthcare/custom_doctype/sales_invoice.py b/healthcare/healthcare/custom_doctype/sales_invoice.py index ea55ad6716..56abe19e8d 100644 --- a/healthcare/healthcare/custom_doctype/sales_invoice.py +++ b/healthcare/healthcare/custom_doctype/sales_invoice.py @@ -40,5 +40,10 @@ def set_healthcare_services(self, checked_values): item_line.reference_dn = checked_item["dn"] if checked_item["description"]: item_line.description = checked_item["description"] + if checked_item["dt"] == "Lab Test": + lab_test = frappe.get_doc("Lab Test", checked_item["dn"]) + item_line.service_unit = lab_test.service_unit + item_line.practitioner = lab_test.practitioner + item_line.medical_department = lab_test.department self.set_missing_values(for_validate=True) diff --git a/healthcare/healthcare/doctype/lab_test/lab_test.json b/healthcare/healthcare/doctype/lab_test/lab_test.json index d78e906b55..6fd2196e7f 100644 --- a/healthcare/healthcare/doctype/lab_test/lab_test.json +++ b/healthcare/healthcare/doctype/lab_test/lab_test.json @@ -33,6 +33,7 @@ "email", "mobile", "c_b", + "service_unit", "practitioner", "practitioner_name", "requesting_department", @@ -579,6 +580,12 @@ "fieldtype": "Table", "label": "Medical Codes", "options": "Codification Table" + }, + { + "fieldname": "service_unit", + "fieldtype": "Link", + "label": "Service Unit", + "options": "Healthcare Service Unit" } ], "is_submittable": 1, @@ -588,7 +595,7 @@ "link_fieldname": "reference_name" } ], - "modified": "2023-02-23 18:56:38.766501", + "modified": "2023-06-21 00:31:52.282467", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test", diff --git a/healthcare/healthcare/doctype/lab_test/lab_test.py b/healthcare/healthcare/doctype/lab_test/lab_test.py index 17f8e4dc47..8a06b102d2 100644 --- a/healthcare/healthcare/doctype/lab_test/lab_test.py +++ b/healthcare/healthcare/doctype/lab_test/lab_test.py @@ -142,7 +142,7 @@ def create_lab_test_from_encounter(encounter): template = get_lab_test_template(item.lab_test_code) if template: lab_test = create_lab_test_doc( - item.invoiced, encounter.practitioner, patient, template, encounter.company + encounter.practitioner, patient, template, encounter.company, item.invoiced ) lab_test.save(ignore_permissions=True) frappe.db.set_value("Lab Prescription", item.name, "lab_test_created", 1) @@ -170,7 +170,7 @@ def create_lab_test_from_invoice(sales_invoice): template = get_lab_test_template(item.item_code) if template: lab_test = create_lab_test_doc( - True, invoice.ref_practitioner, patient, template, invoice.company + invoice.ref_practitioner, patient, template, invoice.company, True, item.service_unit ) if item.reference_dt == "Lab Prescription": lab_test.prescription = item.reference_dn @@ -192,7 +192,9 @@ def get_lab_test_template(item): return False -def create_lab_test_doc(invoiced, practitioner, patient, template, company): +def create_lab_test_doc( + practitioner, patient, template, company, invoiced=False, service_unit=None +): lab_test = frappe.new_doc("Lab Test") lab_test.invoiced = invoiced lab_test.practitioner = practitioner @@ -207,6 +209,7 @@ def create_lab_test_doc(invoiced, practitioner, patient, template, company): lab_test.lab_test_group = template.lab_test_group lab_test.result_date = getdate() lab_test.company = company + lab_test.service_unit = service_unit return lab_test diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index f8fad5b730..eb3bdb5dd5 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -488,6 +488,13 @@ def get_healthcare_service_item(is_inpatient): return service_item +def manage_invoice_validate(doc, method): + if doc.service_unit and len(doc.items): + for item in doc.items: + if not item.service_unit: + item.service_unit = doc.service_unit + + def manage_invoice_submit_cancel(doc, method): if not doc.patient: return diff --git a/healthcare/hooks.py b/healthcare/hooks.py index 2112111e33..046ae411b7 100644 --- a/healthcare/hooks.py +++ b/healthcare/hooks.py @@ -116,6 +116,7 @@ "Sales Invoice": { "on_submit": "healthcare.healthcare.utils.manage_invoice_submit_cancel", "on_cancel": "healthcare.healthcare.utils.manage_invoice_submit_cancel", + "validate": "healthcare.healthcare.utils.manage_invoice_validate", }, "Company": { "after_insert": "healthcare.healthcare.utils.create_healthcare_service_unit_tree_root", diff --git a/healthcare/patches.txt b/healthcare/patches.txt index d26522aa83..b56f19676c 100644 --- a/healthcare/patches.txt +++ b/healthcare/patches.txt @@ -2,8 +2,9 @@ healthcare.patches.v0_0.setup_abdm_custom_fields healthcare.patches.v0_0.set_medical_code_from_field_to_codification_table healthcare.patches.v15_0.set_fee_validity_status +healthcare.patches.v15_0.create_custom_fields_in_sales_invoice_item [post_model_sync] healthcare.patches.v15_0.rename_field_medical_department_in_appoitment_type_service_item healthcare.patches.v15_0.set_default_dynamic_link_dt_for_appointment_type_service_item -healthcare.patches.v15_0.set_allow_booking_for_in_appointment_type \ No newline at end of file +healthcare.patches.v15_0.set_allow_booking_for_in_appointment_type diff --git a/healthcare/patches/v15_0/create_custom_fields_in_sales_invoice_item.py b/healthcare/patches/v15_0/create_custom_fields_in_sales_invoice_item.py new file mode 100644 index 0000000000..b65959dbdf --- /dev/null +++ b/healthcare/patches/v15_0/create_custom_fields_in_sales_invoice_item.py @@ -0,0 +1,43 @@ +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +def execute(): + custom_field = { + "Sales Invoice Item": [ + { + "fieldname": "practitioner", + "label": "Practitioner", + "fieldtype": "Link", + "options": "Healthcare Practitioner", + "insert_after": "reference_dn", + "read_only": True, + }, + { + "fieldname": "medical_department", + "label": "Medical Department", + "fieldtype": "Link", + "options": "Medical Department", + "insert_after": "delivered_qty", + "read_only": True, + }, + { + "fieldname": "service_unit", + "label": "Service Unit", + "fieldtype": "Link", + "options": "Healthcare Service Unit", + "insert_after": "medical_department", + "read_only": True, + }, + ], + "Sales Invoice": [ + { + "fieldname": "service_unit", + "label": "Service Unit", + "fieldtype": "Link", + "options": "Healthcare Service Unit", + "insert_after": "customer_name", + }, + ], + } + + create_custom_fields(custom_field) diff --git a/healthcare/public/js/sales_invoice.js b/healthcare/public/js/sales_invoice.js index 356ea1a98c..809b6abdba 100644 --- a/healthcare/public/js/sales_invoice.js +++ b/healthcare/public/js/sales_invoice.js @@ -32,9 +32,27 @@ frappe.ui.form.on('Sales Invoice', { frm.set_value("customer", ""); frm.set_df_property("customer", "read_only", 0); } + }, + + service_unit: function (frm) { + set_service_unit(frm); + }, + + items_add: function (frm) { + set_service_unit(frm); } }); +var set_service_unit = function (frm) { + if (frm.doc.service_unit && frm.doc.items.length > 0) { + frm.doc.items.forEach((item) => { + if (!item.service_unit) { + frappe.model.set_value(item.doctype, item.name, "service_unit", frm.doc.service_unit); + } + }); + } +}; + var get_healthcare_services_to_invoice = function(frm) { var me = this; let selected_patient = ''; diff --git a/healthcare/setup.py b/healthcare/setup.py index 22601abd6e..2c1ecdd07b 100644 --- a/healthcare/setup.py +++ b/healthcare/setup.py @@ -51,6 +51,13 @@ "options": "Healthcare Practitioner", "insert_after": "customer", }, + { + "fieldname": "service_unit", + "label": "Service Unit", + "fieldtype": "Link", + "options": "Healthcare Service Unit", + "insert_after": "customer_name", + }, ], "Sales Invoice Item": [ { @@ -67,6 +74,30 @@ "options": "reference_dt", "insert_after": "reference_dt", }, + { + "fieldname": "practitioner", + "label": "Practitioner", + "fieldtype": "Link", + "options": "Healthcare Practitioner", + "insert_after": "reference_dn", + "read_only": True, + }, + { + "fieldname": "medical_department", + "label": "Medical Department", + "fieldtype": "Link", + "options": "Medical Department", + "insert_after": "delivered_qty", + "read_only": True, + }, + { + "fieldname": "service_unit", + "label": "Service Unit", + "fieldtype": "Link", + "options": "Healthcare Service Unit", + "insert_after": "medical_department", + "read_only": True, + }, ], "Stock Entry": [ { From cba9ad9b47aaed972b552b77f06d155ae132d50a Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Fri, 11 Aug 2023 16:48:03 +0530 Subject: [PATCH 28/39] fix: Change field name automate_appointment_invoicing to show_payment_popup in Healthcare Settings (cherry picked from commit 99686521871731e3ec9ea14d69e19f232f0a1ba0) --- .../doctype/fee_validity/fee_validity.py | 19 +- .../doctype/fee_validity/test_fee_validity.py | 2 +- .../healthcare_settings.js | 6 +- .../healthcare_settings.json | 10 +- .../patient_appointment.js | 293 +++++++++++------- .../patient_appointment.py | 89 +++--- .../test_patient_appointment.py | 35 ++- .../test_patient_medical_record.py | 2 +- healthcare/healthcare/utils.py | 2 +- healthcare/patches.txt | 1 + .../rename_automate_appointment_invoicing.py | 20 ++ 11 files changed, 297 insertions(+), 182 deletions(-) create mode 100644 healthcare/patches/v15_0/rename_automate_appointment_invoicing.py diff --git a/healthcare/healthcare/doctype/fee_validity/fee_validity.py b/healthcare/healthcare/doctype/fee_validity/fee_validity.py index 4026744ad0..94d14ea85d 100644 --- a/healthcare/healthcare/doctype/fee_validity/fee_validity.py +++ b/healthcare/healthcare/doctype/fee_validity/fee_validity.py @@ -80,6 +80,8 @@ def check_fee_validity(appointment, date=None, practitioner=None): } if appointment.status != "Cancelled": filters["status"] = "Active" + else: + filters["patient_appointment"] = appointment.name validity = frappe.db.exists( "Fee Validity", @@ -92,8 +94,8 @@ def check_fee_validity(appointment, date=None, practitioner=None): return else: validity = get_fee_validity(appointment.get("name"), date) or None - if validity: - return validity + if validity and len(validity): + return frappe.get_doc("Fee Validity", validity[0].get("name")) return validity = frappe.get_doc("Fee Validity", validity) @@ -155,11 +157,12 @@ def manage_fee_validity(appointment): @frappe.whitelist() -def get_fee_validity(appointment_name, date): +def get_fee_validity(appointment_name, date, ignore_status=False): """ Get the fee validity details for the free visit appointment :params appointment_name: Appointment doc name :params date: Schedule date + :params ignore_status: status will not filter in query :return fee validity name and valid_till values of free visit appointments """ if appointment_name: @@ -167,18 +170,22 @@ def get_fee_validity(appointment_name, date): fee_validity = frappe.qb.DocType("Fee Validity") child = frappe.qb.DocType("Fee Validity Reference") - return ( + query = ( frappe.qb.from_(fee_validity) .inner_join(child) .on(fee_validity.name == child.parent) .select(fee_validity.name, fee_validity.valid_till) - .where(fee_validity.status == "Active") .where(fee_validity.start_date <= date) .where(fee_validity.valid_till >= date) .where(fee_validity.patient == appointment_doc.patient) .where(fee_validity.practitioner == appointment_doc.practitioner) .where(child.appointment == appointment_name) - ).run(as_dict=True) + ) + + if not ignore_status: + query = query.where(fee_validity.status == "Active") + + return query.run(as_dict=True) def update_validity_status(): diff --git a/healthcare/healthcare/doctype/fee_validity/test_fee_validity.py b/healthcare/healthcare/doctype/fee_validity/test_fee_validity.py index a7e9d813b9..fba83333c0 100644 --- a/healthcare/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/healthcare/healthcare/doctype/fee_validity/test_fee_validity.py @@ -31,7 +31,7 @@ def test_fee_validity(self): healthcare_settings.enable_free_follow_ups = 1 healthcare_settings.max_visits = 1 healthcare_settings.valid_days = 7 - healthcare_settings.automate_appointment_invoicing = 1 + healthcare_settings.show_payment_popup = 1 healthcare_settings.op_consulting_charge_item = item healthcare_settings.save(ignore_permissions=True) patient, practitioner = create_healthcare_docs() diff --git a/healthcare/healthcare/doctype/healthcare_settings/healthcare_settings.js b/healthcare/healthcare/doctype/healthcare_settings/healthcare_settings.js index 6b0275c8af..96b9813333 100644 --- a/healthcare/healthcare/doctype/healthcare_settings/healthcare_settings.js +++ b/healthcare/healthcare/doctype/healthcare_settings/healthcare_settings.js @@ -64,9 +64,9 @@ frappe.tour['Healthcare Settings'] = [ description: __('If your Healthcare facility bills registrations of Patients, you can check this and set the Registration Fee in the field below. Checking this will create new Patients with a Disabled status by default and will only be enabled after invoicing the Registration Fee.') }, { - fieldname: 'automate_appointment_invoicing', - title: __('Automate Appointment Invoicing'), - description: __('Checking this will automatically create a Sales Invoice whenever an appointment is booked for a Patient.') + fieldname: 'show_payment_popup', + title: __('Show Payment Popup'), + description: __('Checking this will popup to invoice appointment') }, { fieldname: 'validate_nursing_checklists', diff --git a/healthcare/healthcare/doctype/healthcare_settings/healthcare_settings.json b/healthcare/healthcare/doctype/healthcare_settings/healthcare_settings.json index 11dffddaee..583e61b6f3 100644 --- a/healthcare/healthcare/doctype/healthcare_settings/healthcare_settings.json +++ b/healthcare/healthcare/doctype/healthcare_settings/healthcare_settings.json @@ -15,7 +15,7 @@ "column_break_9", "collect_registration_fee", "registration_fee", - "automate_appointment_invoicing", + "show_payment_popup", "enable_free_follow_ups", "max_visits", "valid_days", @@ -231,10 +231,10 @@ }, { "default": "0", - "description": "Manage Appointment Invoice submit and cancel automatically for Patient Encounter", - "fieldname": "automate_appointment_invoicing", + "description": "Checking this will popup dialog for Appointment Invoicing", + "fieldname": "show_payment_popup", "fieldtype": "Check", - "label": "Automate Appointment Invoicing" + "label": "Show Payment Popup" }, { "default": "0", @@ -360,7 +360,7 @@ ], "issingle": 1, "links": [], - "modified": "2023-01-13 17:51:25.440851", + "modified": "2023-08-10 17:51:25.440851", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Settings", diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js index ad72a1d10f..bc9eb869b0 100644 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js @@ -112,6 +112,25 @@ frappe.ui.form.on('Patient Appointment', { frm.save(); }); } + + frm.trigger("make_invoice_button"); + }, + + make_invoice_button: function (frm) { + // add button to invoice when show_payment_popup enabled + if (!frm.is_new() && !frm.doc.invoiced && frm.doc.status != "Cancelled") { + frappe.db.get_single_value("Healthcare Settings", "show_payment_popup").then(async val => { + fee_validity = (await frappe.call( + "healthcare.healthcare.doctype.fee_validity.fee_validity.get_fee_validity", + { "appointment_name": frm.doc.name, "date": frm.doc.appointment_date , "ignore_status": true })).message; + + if (val && !fee_validity.length) { + frm.add_custom_button(__("Make Payment"), function () { + make_payment(frm, val); + }); + } + }); + } }, appointment_for: function(frm) { @@ -150,9 +169,22 @@ frappe.ui.form.on('Patient Appointment', { }, set_book_action: function(frm) { - frm.page.set_primary_action(__('Book'), function() { + frm.page.set_primary_action(__('Book'), async function() { frm.enable_save(); - frm.save(); + await frm.save(); + if (!frm.is_new()) { + await frappe.db.get_single_value("Healthcare Settings", "show_payment_popup").then(val => { + frappe.call({ + method: "healthcare.healthcare.doctype.fee_validity.fee_validity.check_fee_validity", + args: { "appointment": frm.doc }, + callback: (r) => { + if (val && !r.message && !frm.doc.invoiced) { + make_payment(frm, val); + } + } + }); + }); + } }); }, @@ -165,33 +197,7 @@ frappe.ui.form.on('Patient Appointment', { indicator: 'red' }); } else { - frappe.call({ - method: 'healthcare.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd', - args: { 'patient': frm.doc.patient }, - callback: function(data) { - if (data.message == true) { - if (frm.doc.mode_of_payment && frm.doc.paid_amount) { - check_and_set_availability(frm); - } - if (!frm.doc.mode_of_payment) { - frappe.msgprint({ - title: __('Not Allowed'), - message: __('Please select a Mode of Payment first'), - indicator: 'red' - }); - } - if (!frm.doc.paid_amount) { - frappe.msgprint({ - title: __('Not Allowed'), - message: __('Please set the Paid Amount first'), - indicator: 'red' - }); - } - } else { - check_and_set_availability(frm); - } - } - }); + check_and_set_availability(frm); } }); }, @@ -247,33 +253,7 @@ frappe.ui.form.on('Patient Appointment', { indicator: 'red' }); } else { - frappe.call({ - method: 'healthcare.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd', - args: { 'patient': frm.doc.patient }, - callback: function(data) { - if (data.message == true) { - if (frm.doc.mode_of_payment && frm.doc.paid_amount) { - check_and_set_availability(frm); - } - if (!frm.doc.mode_of_payment) { - frappe.msgprint({ - title: __('Not Allowed'), - message: __('Please select a Mode of Payment first'), - indicator: 'red' - }); - } - if (!frm.doc.paid_amount) { - frappe.msgprint({ - title: __('Not Allowed'), - message: __('Please set the Paid Amount first'), - indicator: 'red' - }); - } - } else { - check_and_set_availability(frm); - } - } - }); + check_and_set_availability(frm); } }); }, @@ -331,7 +311,7 @@ frappe.ui.form.on('Patient Appointment', { }, set_payment_details: function(frm) { - frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing').then(val => { + frappe.db.get_single_value('Healthcare Settings', 'show_payment_popup').then(val => { if (val) { frappe.call({ method: 'healthcare.healthcare.utils.get_appointment_billing_item_and_rate', @@ -383,31 +363,28 @@ frappe.ui.form.on('Patient Appointment', { toggle_payment_fields: function(frm) { frappe.call({ - method: 'healthcare.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd', + method: 'healthcare.healthcare.doctype.patient_appointment.patient_appointment.check_payment_reqd', args: { 'patient': frm.doc.patient }, callback: function(data) { if (data.message.fee_validity) { - // if fee validity exists and automated appointment invoicing is enabled, + // if fee validity exists and show payment popup is enabled, // show payment fields as non-mandatory frm.toggle_display('mode_of_payment', 0); frm.toggle_display('paid_amount', 0); frm.toggle_display('billing_item', 0); - frm.toggle_reqd('mode_of_payment', 0); frm.toggle_reqd('paid_amount', 0); frm.toggle_reqd('billing_item', 0); } else if (data.message) { frm.toggle_display('mode_of_payment', 1); frm.toggle_display('paid_amount', 1); frm.toggle_display('billing_item', 1); - frm.toggle_reqd('mode_of_payment', 1); frm.toggle_reqd('paid_amount', 1); frm.toggle_reqd('billing_item', 1); } else { - // if automated appointment invoicing is disabled, hide fields + // if show payment popup is disabled, hide fields frm.toggle_display('mode_of_payment', data.message ? 1 : 0); frm.toggle_display('paid_amount', data.message ? 1 : 0); frm.toggle_display('billing_item', data.message ? 1 : 0); - frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0); frm.toggle_reqd('paid_amount', data.message ? 1 : 0); frm.toggle_reqd('billing_item', data.message ? 1 : 0); } @@ -466,13 +443,9 @@ let check_and_set_availability = function(frm) { { fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date', min_date: new Date(frappe.datetime.get_today()) }, { fieldtype: 'Section Break' }, { fieldtype: 'HTML', fieldname: 'available_slots' }, - { fieldtype: 'Section Break', fieldname: 'payment_section', label: 'Payment Details', hidden: 1 }, - { fieldtype: 'Link', options: 'Mode of Payment', fieldname: 'mode_of_payment', label: 'Mode of Payment' }, - { fieldtype: 'Column Break' }, - { fieldtype: 'Currency', fieldname: 'consultation_charge', label: 'Consultation Charge', read_only: 1 }, ], primary_action_label: __('Book'), - primary_action: function() { + primary_action: async function() { frm.set_value('appointment_time', selected_slot); add_video_conferencing = add_video_conferencing && !d.$wrapper.find(".opt-out-check").is(":checked") && !overlap_appointments @@ -481,13 +454,11 @@ let check_and_set_availability = function(frm) { if (!frm.doc.duration) { frm.set_value('duration', duration); } + let practitioner = frm.doc.practitioner; frm.set_value('practitioner', d.get_value('practitioner')); frm.set_value('department', d.get_value('department')); frm.set_value('appointment_date', d.get_value('appointment_date')); - if (d.get_value('mode_of_payment') != frm.doc.mode_of_payment) { - frm.set_value('mode_of_payment', d.get_value('mode_of_payment')); - }; frm.set_value('appointment_based_on_check_in', appointment_based_on_check_in) if (service_unit) { @@ -496,7 +467,25 @@ let check_and_set_availability = function(frm) { d.hide(); frm.enable_save(); - frm.save(); + await frm.save(); + if (!frm.is_new() && (!practitioner || practitioner == d.get_value('practitioner'))) { + await frappe.db.get_single_value("Healthcare Settings", "show_payment_popup").then(val => { + frappe.call({ + method: "healthcare.healthcare.doctype.fee_validity.fee_validity.check_fee_validity", + args: { "appointment": frm.doc }, + callback: (r) => { + if (val && !r.message && !frm.doc.invoiced) { + make_payment(frm, val); + } else { + frappe.call({ + method: "healthcare.healthcare.doctype.patient_appointment.patient_appointment.update_fee_validity", + args: { "appointment": frm.doc } + }); + } + } + }); + }); + } d.get_primary_btn().attr('disabled', true); } }); @@ -505,7 +494,6 @@ let check_and_set_availability = function(frm) { 'department': frm.doc.department, 'practitioner': frm.doc.practitioner, 'appointment_date': frm.doc.appointment_date, - 'mode_of_payment': frm.doc.mode_of_payment, }); let selected_department = frm.doc.department; @@ -537,57 +525,17 @@ let check_and_set_availability = function(frm) { d.fields_dict['appointment_date'].df.onchange = () => { show_slots(d, fd); - validate_fee_validity(frm, d); }; d.fields_dict['practitioner'].df.onchange = () => { if (d.get_value('practitioner') && d.get_value('practitioner') != selected_practitioner) { selected_practitioner = d.get_value('practitioner'); show_slots(d, fd); - validate_fee_validity(frm, d); } }; d.show(); } - function validate_fee_validity(frm, d) { - var section_field = d.get_field("payment_section"); - var payment_field = d.get_field("mode_of_payment"); - section_field.df.hidden = 1; - payment_field.df.reqd = 0; - - if (d.get_value('appointment_date') && !frm.doc.invoiced) { - frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups').then(async function (val) { - if (val) { - fee_validity = (await frappe.call( - 'healthcare.healthcare.doctype.fee_validity.fee_validity.check_fee_validity', - { - appointment: frm.doc, - date: d.get_value('appointment_date'), - practitioner: d.get_value('practitioner') - } - )).message || null; - if (!fee_validity) { - payment_field.df.reqd = 1; - section_field.df.hidden = 0; - - let payment_details = (await frappe.call( - 'healthcare.healthcare.utils.get_appointment_billing_item_and_rate', - { - doc: frm.doc - } - )).message; - d.set_value('consultation_charge', payment_details.practitioner_charge); - payment_field.refresh(); - section_field.refresh(); - } - } - }); - } - payment_field.refresh(); - section_field.refresh(); - } - function show_slots(d, fd) { if (d.get_value('appointment_date') && d.get_value('practitioner')) { fd.available_slots.html(''); @@ -686,7 +634,7 @@ let check_and_set_availability = function(frm) { } else if (fee_validity != 'Disabled') { slot_html += ` - ${__('Patient has no fee validity, need to be invoiced')} + ${__('Patient has no fee validity')}
`; } @@ -960,3 +908,120 @@ let calculate_age = function(birth) { let years = age.getFullYear() - 1970; return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`; }; + +let make_payment = function (frm, automate_invoicing) { + if (automate_invoicing) { + make_registration (frm, automate_invoicing); + } + + function make_registration (frm, automate_invoicing) { + if (automate_invoicing == true && !frm.doc.paid_amount) { + frappe.throw({ + title: __("Not Allowed"), + message: __("Please set the Paid Amount first"), + }); + } + + let fields = [ + { + label: "Patient", + fieldname: "patient", + fieldtype: "Data", + read_only: true, + }, + { + label: "Mode of Payment", + fieldname: "mode_of_payment", + fieldtype: "Link", + options: "Mode of Payment", + reqd: 1, + }, + { + label: "Consultation Charge", + fieldname: "consultation_charge", + fieldtype: "Currency", + read_only: true, + } + ]; + + if (frm.doc.appointment_for == "Practitioner") { + pract_dict = { + label: "Practitioner", + fieldname: "practitioner", + fieldtype: "Data", + read_only: true, + }; + fields.splice(1, 0, pract_dict); + } else if (frm.doc.appointment_for == "Service Unit") { + su_dict = { + label: "Service Unit", + fieldname: "service_unit", + fieldtype: "Data", + read_only: true, + }; + fields.splice(1, 0, su_dict); + } else if (frm.doc.appointment_for == "Department") { + dept_dict = { + label: "Department", + fieldname: "department", + fieldtype: "Data", + read_only: true, + }; + fields.splice(1, 0, dept_dict); + } + + if (automate_invoicing) { + show_payment_dialog(frm, fields); + } + } + + function show_payment_dialog(frm, fields) { + let d = new frappe.ui.Dialog({ + title: "Enter Payment Details", + fields: fields, + primary_action_label: "Create Invoice", + primary_action(values) { + frm.set_value("mode_of_payment", values.mode_of_payment) + frm.save(); + frappe.call({ + method: "healthcare.healthcare.doctype.patient_appointment.patient_appointment.invoice_appointment", + args: { "appointment_name": frm.doc.name }, + callback: async function (data) { + if (!data.exc) { + await frm.reload_doc(); + if (frm.doc.ref_sales_invoice) { + d.get_primary_btn().attr("disabled", true); + d.get_secondary_btn().attr("disabled", false); + } + } + } + }); + }, + secondary_action_label: __(` + + `), + secondary_action() { + window.open("/app/print/Sales Invoice/" + frm.doc.ref_sales_invoice, "_blank"); + d.hide(); + } + }); + d.get_secondary_btn().attr("disabled", true); + d.set_values({ + "patient": frm.doc.patient_name, + "consultation_charge": frm.doc.paid_amount, + }); + + if (frm.doc.appointment_for == "Practitioner") { + d.set_value("practitioner", frm.doc.practitioner_name); + } else if (frm.doc.appointment_for == "Service Unit") { + d.set_value("service_unit", frm.doc.service_unit); + } else if (frm.doc.appointment_for == "Department") { + d.set_value("department", frm.doc.department); + } + + if (frm.doc.mode_of_payment) { + d.set_value("mode_of_payment", frm.doc.mode_of_payment); + } + d.show(); + } +}; diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py index bd874cc99d..aa8acc775d 100755 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py @@ -47,8 +47,11 @@ def validate(self): self.set_postition_in_queue() def on_update(self): - invoice_appointment(self) - self.update_fee_validity() + if ( + not frappe.db.get_single_value("Healthcare Settings", "show_payment_popup") + or not self.practitioner + ): + update_fee_validity(self) def after_insert(self): self.update_prescription_details() @@ -234,14 +237,14 @@ def set_appointment_datetime(self): ) def set_payment_details(self): - if frappe.db.get_single_value("Healthcare Settings", "automate_appointment_invoicing"): + if frappe.db.get_single_value("Healthcare Settings", "show_payment_popup"): details = get_appointment_billing_item_and_rate(self) self.db_set("billing_item", details.get("service_item")) if not self.paid_amount: self.db_set("paid_amount", details.get("practitioner_charge")) def validate_customer_created(self): - if frappe.db.get_single_value("Healthcare Settings", "automate_appointment_invoicing"): + if frappe.db.get_single_value("Healthcare Settings", "show_payment_popup"): if not frappe.db.get_value("Patient", self.patient, "customer"): msg = _("Please set a Customer linked to the Patient") msg += " {0}".format(self.patient) @@ -259,21 +262,6 @@ def update_prescription_details(self): if comments: frappe.db.set_value("Patient Appointment", self.name, "notes", comments) - def update_fee_validity(self): - if ( - not frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") - or not self.practitioner - ): - return - - fee_validity = manage_fee_validity(self) - if fee_validity: - frappe.msgprint( - _("{0} has fee validity till {1}").format( - frappe.bold(self.patient_name), format_date(fee_validity.valid_till) - ) - ) - def insert_calendar_event(self): if not self.practitioner: return @@ -378,12 +366,14 @@ def set_postition_in_queue(self): @frappe.whitelist() -def check_payment_fields_reqd(patient): - automate_invoicing = frappe.db.get_single_value( - "Healthcare Settings", "automate_appointment_invoicing" - ) +def check_payment_reqd(patient): + """ + return True if patient need to be invoiced when show_payment_popup enabled or have no fee validity + return False show_payment_popup is disabled + """ + show_payment_popup = frappe.db.get_single_value("Healthcare Settings", "show_payment_popup") free_follow_ups = frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") - if automate_invoicing: + if show_payment_popup: if free_follow_ups: fee_validity = frappe.db.exists("Fee Validity", {"patient": patient, "status": "Active"}) if fee_validity: @@ -392,17 +382,13 @@ def check_payment_fields_reqd(patient): return False -def invoice_appointment(appointment_doc): - automate_invoicing = frappe.db.get_single_value( - "Healthcare Settings", "automate_appointment_invoicing" - ) - appointment_invoiced = frappe.db.get_value( - "Patient Appointment", appointment_doc.name, "invoiced" - ) - enable_free_follow_ups = frappe.db.get_single_value( - "Healthcare Settings", "enable_free_follow_ups" - ) - if enable_free_follow_ups: +@frappe.whitelist() +def invoice_appointment(appointment_name): + appointment_doc = frappe.get_doc("Patient Appointment", appointment_name) + show_payment_popup = frappe.db.get_single_value("Healthcare Settings", "show_payment_popup") + free_follow_ups = frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") + + if free_follow_ups: fee_validity = check_fee_validity(appointment_doc) if fee_validity and fee_validity.status != "Active": @@ -413,8 +399,9 @@ def invoice_appointment(appointment_doc): else: fee_validity = None - if automate_invoicing and not appointment_invoiced and not fee_validity: + if show_payment_popup and not appointment_doc.invoiced and not fee_validity: create_sales_invoice(appointment_doc) + update_fee_validity(appointment_doc) def create_sales_invoice(appointment_doc): @@ -444,11 +431,37 @@ def create_sales_invoice(appointment_doc): frappe.db.set_value( "Patient Appointment", appointment_doc.name, - {"invoiced": 1, "ref_sales_invoice": sales_invoice.name}, + { + "invoiced": 1, + "ref_sales_invoice": sales_invoice.name, + "paid_amount": appointment_doc.paid_amount, + }, ) appointment_doc.reload() +@frappe.whitelist() +def update_fee_validity(appointment): + if isinstance(appointment, str): + appointment = json.loads(appointment) + appointment = frappe.get_doc(appointment) + + if ( + not frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") + or not appointment.practitioner + ): + return + + fee_validity = manage_fee_validity(appointment) + if fee_validity: + frappe.msgprint( + _("{0} has fee validity till {1}").format( + frappe.bold(appointment.patient_name), format_date(fee_validity.valid_till) + ), + alert=True, + ) + + def check_is_new_patient(patient, name=None): filters = {"patient": patient, "status": ("!=", "Cancelled")} if name: @@ -505,7 +518,7 @@ def cancel_appointment(appointment_id): def cancel_sales_invoice(sales_invoice): - if frappe.db.get_single_value("Healthcare Settings", "automate_appointment_invoicing"): + if frappe.db.get_single_value("Healthcare Settings", "show_payment_popup"): if len(sales_invoice.items) == 1: sales_invoice.cancel() return True diff --git a/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py index 51256bcb27..059546eabb 100644 --- a/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -12,7 +12,8 @@ from healthcare.healthcare.doctype.patient_appointment.patient_appointment import ( check_is_new_patient, - check_payment_fields_reqd, + check_payment_reqd, + invoice_appointment, make_encounter, update_status, ) @@ -31,7 +32,7 @@ def setUp(self): def test_status(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 0) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 0) appointment = create_appointment(patient, practitioner, nowdate()) self.assertEqual(appointment.status, "Open") appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2)) @@ -45,7 +46,7 @@ def test_status(self): def test_start_encounter(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice=1) appointment.reload() self.assertEqual(appointment.invoiced, 1) @@ -62,11 +63,11 @@ def test_start_encounter(self): def test_auto_invoicing(self): patient, practitioner = create_healthcare_docs() frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) - frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 0) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 0) appointment = create_appointment(patient, practitioner, nowdate()) self.assertEqual(frappe.db.get_value("Patient Appointment", appointment.name, "invoiced"), 0) - frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), invoice=1) self.assertEqual(frappe.db.get_value("Patient Appointment", appointment.name, "invoiced"), 1) sales_invoice_name = frappe.db.get_value( @@ -95,7 +96,7 @@ def test_auto_invoicing_based_on_practitioner_department(self): ) medical_department = create_medical_department() frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) - frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 1) appointment_type = create_appointment_type( {"medical_department": medical_department, "op_consulting_charge": 200} ) @@ -124,7 +125,7 @@ def test_auto_invoicing_based_on_practitioner_department(self): def test_auto_invoicing_based_on_department(self): frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 1) - frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 1) item = create_healthcare_service_items() department_name = create_medical_department(id=111) # "_Test Medical Department 111" items = [ @@ -151,6 +152,9 @@ def test_auto_invoicing_based_on_department(self): appointment.company = "_Test Company" appointment.save(ignore_permissions=True) + if frappe.db.get_single_value("Healthcare Settings", "show_payment_popup"): + invoice_appointment(appointment.name) + appointment.reload() self.assertEqual(appointment.invoiced, 1) self.assertEqual(appointment.billing_item, item) @@ -163,7 +167,7 @@ def test_auto_invoicing_based_on_department(self): def test_auto_invoicing_based_on_service_unit(self): frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) - frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 1) item = create_healthcare_service_items() service_unit_type = create_service_unit_type(id=11, allow_appointments=1) service_unit = create_service_unit( @@ -194,6 +198,9 @@ def test_auto_invoicing_based_on_service_unit(self): appointment.company = "_Test Company" appointment.save(ignore_permissions=True) + if frappe.db.get_single_value("Healthcare Settings", "show_payment_popup"): + invoice_appointment(appointment.name) + appointment.reload() self.assertEqual(appointment.invoiced, 1) self.assertEqual(appointment.billing_item, item) @@ -215,7 +222,7 @@ def test_auto_invoicing_according_to_appointment_type_charge(self): }, ) frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) - frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 1) item = create_healthcare_service_items() items = [{"op_consulting_charge_item": item, "op_consulting_charge": 300}] @@ -256,7 +263,7 @@ def test_appointment_cancel(self): self.assertEqual(frappe.db.get_value("Fee Validity", fee_validity, "visited"), 0) frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) - frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1) update_status(appointment.name, "Cancelled") # check invoice cancelled @@ -338,18 +345,18 @@ def test_invalid_healthcare_service_unit_validation(self): def test_payment_should_be_mandatory_for_new_patient_appointment(self): frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 1) - frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 1) frappe.db.set_single_value("Healthcare Settings", "max_visits", 3) frappe.db.set_single_value("Healthcare Settings", "valid_days", 30) patient = create_patient() assert check_is_new_patient(patient) - payment_required = check_payment_fields_reqd(patient) + payment_required = check_payment_reqd(patient) assert payment_required is True def test_sales_invoice_should_be_generated_for_new_patient_appointment(self): patient, practitioner = create_healthcare_docs() - frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 1) invoice_count = frappe.db.count("Sales Invoice") assert check_is_new_patient(patient) @@ -618,6 +625,8 @@ def create_appointment( appointment.appointment_time = appointment_time if save: appointment.save(ignore_permissions=True) + if invoice or frappe.db.get_single_value("Healthcare Settings", "show_payment_popup"): + invoice_appointment(appointment.name) return appointment diff --git a/healthcare/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/healthcare/healthcare/doctype/patient_medical_record/test_patient_medical_record.py index 1440dd1913..8f57d31bf1 100644 --- a/healthcare/healthcare/doctype/patient_medical_record/test_patient_medical_record.py +++ b/healthcare/healthcare/doctype/patient_medical_record/test_patient_medical_record.py @@ -19,7 +19,7 @@ class TestPatientMedicalRecord(FrappeTestCase): def setUp(self): frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) - frappe.db.set_single_value("Healthcare Settings", "automate_appointment_invoicing", 1) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 1) make_pos_profile() def test_medical_record(self): diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index eb3bdb5dd5..027eda9868 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -510,7 +510,7 @@ def manage_invoice_submit_cancel(doc, method): create_multiple("Sales Invoice", doc.name) if ( - not frappe.db.get_single_value("Healthcare Settings", "automate_appointment_invoicing") + not frappe.db.get_single_value("Healthcare Settings", "show_payment_popup") and frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") and doc.items ): diff --git a/healthcare/patches.txt b/healthcare/patches.txt index b56f19676c..16ebcbc962 100644 --- a/healthcare/patches.txt +++ b/healthcare/patches.txt @@ -3,6 +3,7 @@ healthcare.patches.v0_0.setup_abdm_custom_fields healthcare.patches.v0_0.set_medical_code_from_field_to_codification_table healthcare.patches.v15_0.set_fee_validity_status healthcare.patches.v15_0.create_custom_fields_in_sales_invoice_item +healthcare.patches.v15_0.rename_automate_appointment_invoicing [post_model_sync] healthcare.patches.v15_0.rename_field_medical_department_in_appoitment_type_service_item diff --git a/healthcare/patches/v15_0/rename_automate_appointment_invoicing.py b/healthcare/patches/v15_0/rename_automate_appointment_invoicing.py new file mode 100644 index 0000000000..431af52703 --- /dev/null +++ b/healthcare/patches/v15_0/rename_automate_appointment_invoicing.py @@ -0,0 +1,20 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + frappe.reload_doc("healthcare", "doctype", frappe.scrub("Healthcare Settings")) + + try: + # Rename the field + rename_field("Healthcare Settings", "automate_appointment_invoicing", "show_payment_popup") + + # Copy the value + old_value = frappe.db.get_single_value("Healthcare Settings", "show_payment_popup") + frappe.db.set_single_value( + "Healthcare Settings", "show_payment_popup", 1 if old_value == 1 else 0 + ) + + except Exception as e: + if e.args and e.args[0]: + raise From 4a4ce778b56fb0fa3bc73030f795028b7f8c3307 Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Fri, 11 Aug 2023 18:01:03 +0530 Subject: [PATCH 29/39] fix: Remove duplicate event in Patient Appointment (cherry picked from commit f0587f8bb4b6cdb96d06b927f2a977aecfff4411) --- .../patient_appointment.js | 56 ------------------- 1 file changed, 56 deletions(-) diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js index bc9eb869b0..fff52e4b15 100644 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js @@ -202,62 +202,6 @@ frappe.ui.form.on('Patient Appointment', { }); }, - appointment_for: function(frm) { - if (frm.doc.appointment_for == 'Practitioner') { - if (!frm.doc.practitioner) { - frm.set_value('department', ''); - } - frm.set_value('service_unit', ''); - frm.trigger('set_check_availability_action'); - } else if (frm.doc.appointment_for == 'Service Unit') { - frm.set_value({ - 'practitioner': '', - 'practitioner_name': '', - 'department': '', - }); - frm.trigger('set_book_action'); - } else if (frm.doc.appointment_for == 'Department') { - frm.set_value({ - 'practitioner': '', - 'practitioner_name': '', - 'service_unit': '', - }); - frm.trigger('set_book_action'); - } else { - if (frm.doc.appointment_for == 'Department') { - frm.set_value('service_unit', ''); - } - frm.set_value({ - 'practitioner': '', - 'practitioner_name': '', - 'department': '', - 'service_unit': '', - }); - frm.page.clear_primary_action(); - } - }, - - set_book_action: function(frm) { - frm.page.set_primary_action(__('Book'), function() { - frm.enable_save(); - frm.save(); - }); - }, - - set_check_availability_action: function(frm) { - frm.page.set_primary_action(__('Check Availability'), function() { - if (!frm.doc.patient) { - frappe.msgprint({ - title: __('Not Allowed'), - message: __('Please select Patient first'), - indicator: 'red' - }); - } else { - check_and_set_availability(frm); - } - }); - }, - patient: function(frm) { if (frm.doc.patient) { frm.trigger('toggle_payment_fields'); From d46e417a6aea348ed0a08427eab8dfe95c4eaba1 Mon Sep 17 00:00:00 2001 From: Syed Mujeer Hashmi Date: Wed, 13 Sep 2023 22:23:12 +0530 Subject: [PATCH 30/39] fix: Define variables before access in patient appointment (cherry picked from commit bc9de0a477d9077959d433071163d2e48528ee1f) --- .../doctype/patient_appointment/patient_appointment.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js index fff52e4b15..4a9acd311b 100644 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js @@ -120,7 +120,7 @@ frappe.ui.form.on('Patient Appointment', { // add button to invoice when show_payment_popup enabled if (!frm.is_new() && !frm.doc.invoiced && frm.doc.status != "Cancelled") { frappe.db.get_single_value("Healthcare Settings", "show_payment_popup").then(async val => { - fee_validity = (await frappe.call( + let fee_validity = (await frappe.call( "healthcare.healthcare.doctype.fee_validity.fee_validity.get_fee_validity", { "appointment_name": frm.doc.name, "date": frm.doc.appointment_date , "ignore_status": true })).message; @@ -889,7 +889,7 @@ let make_payment = function (frm, automate_invoicing) { ]; if (frm.doc.appointment_for == "Practitioner") { - pract_dict = { + let pract_dict = { label: "Practitioner", fieldname: "practitioner", fieldtype: "Data", @@ -897,7 +897,7 @@ let make_payment = function (frm, automate_invoicing) { }; fields.splice(1, 0, pract_dict); } else if (frm.doc.appointment_for == "Service Unit") { - su_dict = { + let su_dict = { label: "Service Unit", fieldname: "service_unit", fieldtype: "Data", @@ -905,7 +905,7 @@ let make_payment = function (frm, automate_invoicing) { }; fields.splice(1, 0, su_dict); } else if (frm.doc.appointment_for == "Department") { - dept_dict = { + let dept_dict = { label: "Department", fieldname: "department", fieldtype: "Data", From be483425606542ae2397a59ceee3ce4d02ec1403 Mon Sep 17 00:00:00 2001 From: Syed Mujeer Hashmi Date: Wed, 20 Sep 2023 15:42:36 +0530 Subject: [PATCH 31/39] fix: Key error during schedule discharge The checkout time for inpatient occupancy should also be based on scheduled order datetime. --- .../inpatient_record/inpatient_record.json | 44 ++++++++----------- .../inpatient_record/inpatient_record.py | 11 +++-- .../patient_encounter/patient_encounter.js | 4 +- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/healthcare/healthcare/doctype/inpatient_record/inpatient_record.json b/healthcare/healthcare/doctype/inpatient_record/inpatient_record.json index b8a00a1475..6d394dcc65 100644 --- a/healthcare/healthcare/doctype/inpatient_record/inpatient_record.json +++ b/healthcare/healthcare/doctype/inpatient_record/inpatient_record.json @@ -52,7 +52,7 @@ "inpatient_occupancies", "btn_transfer", "sb_discharge_details", - "discharge_ordered_date", + "discharge_ordered_datetime", "discharge_practitioner", "discharge_encounter", "discharge_datetime", @@ -387,10 +387,10 @@ "label": "Discharge Instructions" }, { - "fieldname": "discharge_ordered_date", - "fieldtype": "Date", + "fieldname": "discharge_ordered_datetime", + "fieldtype": "Datetime", "in_list_view": 1, - "label": "Discharge Ordered Date", + "label": "Discharge Ordered Datetime", "read_only": 1 }, { @@ -433,6 +433,18 @@ "fieldtype": "Link", "label": "Admission Nursing Checklist Template", "options": "Nursing Checklist Template" + }, + { + "collapsible": 1, + "fieldname": "cancellation_details_section", + "fieldtype": "Section Break", + "label": "Cancellation Details" + }, + { + "fieldname": "reason_for_cancellation", + "fieldtype": "Small Text", + "label": "Reason for Cancellation", + "read_only": 1 } ], "index_web_pages_for_search": 1, @@ -471,23 +483,9 @@ "group": "Nursing", "link_doctype": "Vital Signs", "link_fieldname": "inpatient_record" - }, - { - "collapsible": 1, - "fieldname": "cancellation_details_section", - "fieldtype": "Section Break", - "label": "Cancellation Details" - }, - { - "fieldname": "reason_for_cancellation", - "fieldtype": "Small Text", - "label": "Reason for Cancellation", - "read_only": 1 } ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2022-04-28 17:26:28.973945", + "modified": "2023-09-20 17:57:02.408982", "modified_by": "Administrator", "module": "Healthcare", "name": "Inpatient Record", @@ -574,12 +572,6 @@ "share": 1 } ], - "links": [ - { - "link_doctype": "Nursing Task", - "link_fieldname": "reference_name" - } - ], "restrict_to_domain": "Healthcare", "search_fields": "patient", "sort_field": "modified", @@ -587,4 +579,4 @@ "states": [], "title_field": "patient", "track_changes": 1 -} +} \ No newline at end of file diff --git a/healthcare/healthcare/doctype/inpatient_record/inpatient_record.py b/healthcare/healthcare/doctype/inpatient_record/inpatient_record.py index 8d39e52bbf..3959a400b0 100644 --- a/healthcare/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/healthcare/healthcare/doctype/inpatient_record/inpatient_record.py @@ -44,7 +44,7 @@ def validate(self): def validate_dates(self): if (getdate(self.expected_discharge) < getdate(self.scheduled_date)) or ( - getdate(self.discharge_ordered_date) < getdate(self.scheduled_date) + getdate(self.discharge_ordered_datetime) < getdate(self.scheduled_date) ): frappe.throw(_("Expected and Discharge dates cannot be less than Admission Schedule date")) @@ -155,7 +155,7 @@ def schedule_discharge(args): if inpatient_record_id: inpatient_record = frappe.get_doc("Inpatient Record", inpatient_record_id) - check_out_inpatient(inpatient_record) + check_out_inpatient(inpatient_record, discharge_order) set_details_from_ip_order(inpatient_record, discharge_order) inpatient_record.status = "Discharge Scheduled" inpatient_record.save(ignore_permissions=True) @@ -191,12 +191,15 @@ def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_chi table.set(df.fieldname, item.get(df.fieldname)) -def check_out_inpatient(inpatient_record): +def check_out_inpatient(inpatient_record, discharge_order): if inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies: if inpatient_occupancy.left != 1: inpatient_occupancy.left = True - inpatient_occupancy.check_out = now_datetime() + if "discharge_ordered_datetime" in discharge_order: + inpatient_occupancy.check_out = discharge_order["discharge_ordered_datetime"] + else: + inpatient_occupancy.check_out = now_datetime() frappe.db.set_value( "Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant" ) diff --git a/healthcare/healthcare/doctype/patient_encounter/patient_encounter.js b/healthcare/healthcare/doctype/patient_encounter/patient_encounter.js index 4ce3b4109c..2125c5aadb 100644 --- a/healthcare/healthcare/doctype/patient_encounter/patient_encounter.js +++ b/healthcare/healthcare/doctype/patient_encounter/patient_encounter.js @@ -334,7 +334,7 @@ var schedule_discharge = function(frm) { var dialog = new frappe.ui.Dialog ({ title: 'Inpatient Discharge', fields: [ - {fieldtype: 'Date', label: 'Discharge Ordered Date', fieldname: 'discharge_ordered_date', default: 'Today', read_only: 1}, + {fieldtype: 'Datetime', label: 'Discharge Ordered Datetime', fieldname: 'discharge_ordered_datetime', default: 'Today', read_only: 1}, {fieldtype: 'Date', label: 'Followup Date', fieldname: 'followup_date'}, {fieldtype: 'Link', label: 'Nursing Checklist Template', options: 'Nursing Checklist Template', fieldname: 'discharge_nursing_checklist_template'}, {fieldtype: 'Column Break'}, @@ -348,7 +348,7 @@ var schedule_discharge = function(frm) { patient: frm.doc.patient, discharge_encounter: frm.doc.name, discharge_practitioner: frm.doc.practitioner, - discharge_ordered_date: dialog.get_value('discharge_ordered_date'), + discharge_ordered_datetime: dialog.get_value('discharge_ordered_datetime'), followup_date: dialog.get_value('followup_date'), discharge_instructions: dialog.get_value('discharge_instructions'), discharge_note: dialog.get_value('discharge_note'), From 7921a63c4f65cbab9d79b3624bfcb5e67c978b48 Mon Sep 17 00:00:00 2001 From: Syed Mujeer Hashmi Date: Sun, 24 Sep 2023 00:14:00 +0530 Subject: [PATCH 32/39] fix: Linter issues for failing semgrep rule Signed-off-by: Syed Mujeer Hashmi --- .../doctype/inpatient_record/inpatient_record.js | 4 ++-- .../doctype/inpatient_record/inpatient_record.py | 4 ++-- .../doctype/patient_encounter/patient_encounter.js | 9 ++++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/healthcare/healthcare/doctype/inpatient_record/inpatient_record.js b/healthcare/healthcare/doctype/inpatient_record/inpatient_record.js index 00ef14565b..ae7f604c4a 100644 --- a/healthcare/healthcare/doctype/inpatient_record/inpatient_record.js +++ b/healthcare/healthcare/doctype/inpatient_record/inpatient_record.js @@ -265,7 +265,7 @@ var schedule_discharge = function(frm) { ], primary_action_label: __('Order Discharge'), primary_action : function() { - var args = { + var discharge_details = { patient: frm.doc.patient, discharge_practitioner: dialog.get_value('discharge_practitioner'), discharge_ordered_datetime: dialog.get_value('discharge_ordered_datetime'), @@ -275,7 +275,7 @@ var schedule_discharge = function(frm) { } frappe.call ({ method: 'healthcare.healthcare.doctype.inpatient_record.inpatient_record.schedule_discharge', - args: {args}, + args: {discharge_details}, callback: function(data) { if(!data.exc){ frm.reload_doc(); diff --git a/healthcare/healthcare/doctype/inpatient_record/inpatient_record.py b/healthcare/healthcare/doctype/inpatient_record/inpatient_record.py index 3959a400b0..b609fc70f1 100644 --- a/healthcare/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/healthcare/healthcare/doctype/inpatient_record/inpatient_record.py @@ -146,8 +146,8 @@ def schedule_inpatient(args): @frappe.whitelist() -def schedule_discharge(args): - discharge_order = json.loads(args) +def schedule_discharge(discharge_details): + discharge_order = json.loads(discharge_details) inpatient_record_id = frappe.db.get_value( "Patient", discharge_order["patient"], "inpatient_record" ) diff --git a/healthcare/healthcare/doctype/patient_encounter/patient_encounter.js b/healthcare/healthcare/doctype/patient_encounter/patient_encounter.js index 2125c5aadb..ca8b4f0a11 100644 --- a/healthcare/healthcare/doctype/patient_encounter/patient_encounter.js +++ b/healthcare/healthcare/doctype/patient_encounter/patient_encounter.js @@ -3,8 +3,7 @@ frappe.ui.form.on('Patient Encounter', { onload: function(frm) { - if (!frm.doc.__islocal && frm.doc.docstatus === 1 && - frm.doc.inpatient_status == 'Admission Scheduled') { + if (!frm.doc.__islocal && frm.doc.docstatus === 1) { frappe.db.get_value('Inpatient Record', frm.doc.inpatient_record, ['admission_encounter', 'status']).then(r => { if (r.message) { @@ -334,7 +333,7 @@ var schedule_discharge = function(frm) { var dialog = new frappe.ui.Dialog ({ title: 'Inpatient Discharge', fields: [ - {fieldtype: 'Datetime', label: 'Discharge Ordered Datetime', fieldname: 'discharge_ordered_datetime', default: 'Today', read_only: 1}, + {fieldtype: 'Datetime', label: 'Discharge Ordered Datetime', fieldname: 'discharge_ordered_datetime', default: frappe.datetime.now_datetime()}, {fieldtype: 'Date', label: 'Followup Date', fieldname: 'followup_date'}, {fieldtype: 'Link', label: 'Nursing Checklist Template', options: 'Nursing Checklist Template', fieldname: 'discharge_nursing_checklist_template'}, {fieldtype: 'Column Break'}, @@ -344,7 +343,7 @@ var schedule_discharge = function(frm) { ], primary_action_label: __('Order Discharge'), primary_action : function() { - var args = { + var discharge_details = { patient: frm.doc.patient, discharge_encounter: frm.doc.name, discharge_practitioner: frm.doc.practitioner, @@ -356,7 +355,7 @@ var schedule_discharge = function(frm) { } frappe.call ({ method: 'healthcare.healthcare.doctype.inpatient_record.inpatient_record.schedule_discharge', - args: {args}, + args: {discharge_details}, callback: function(data) { if(!data.exc){ frm.reload_doc(); From e76b7ac3443b77a0650d299a9228c24626f72cfe Mon Sep 17 00:00:00 2001 From: Akash Date: Mon, 2 Oct 2023 14:01:13 +0530 Subject: [PATCH 33/39] fix: Patient Appointment - position in queue repetition issue (cherry picked from commit c6d0ead8c38f693391993fef15fd705a97056f7a) --- .../patient_appointment.py | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py index aa8acc775d..c0db56ad42 100755 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py @@ -352,17 +352,27 @@ def update_event(self): self.google_meet_link = event_doc.google_meet_link def set_postition_in_queue(self): + from frappe.query_builder.functions import Max + if self.status == "Checked In" and not self.position_in_queue: - app_count = frappe.db.count( - "Patient Appointment", - { - "status": "Checked In", - "practitioner": self.practitioner, - "service_unit": self.service_unit, - "appointment_time": self.appointment_time, - }, - ) - self.position_in_queue = app_count + 1 + appointment = frappe.qb.DocType("Patient Appointment") + position = ( + frappe.qb.from_(appointment) + .select( + Max(appointment.position_in_queue).as_("max_position"), + ) + .where( + (appointment.status == "Checked In") + & (appointment.practitioner == self.practitioner) + & (appointment.service_unit == self.service_unit) + & (appointment.appointment_time == self.appointment_time) + ) + ).run(as_dict=True)[0] + position_in_queue = 1 + if position and position.get("max_position"): + position_in_queue = position.get("max_position") + 1 + + self.position_in_queue = position_in_queue @frappe.whitelist() From ae5f6e66295e675f20b2bd6ec45377b46115e365 Mon Sep 17 00:00:00 2001 From: Mohammed Irfan Date: Sat, 28 Oct 2023 13:09:48 +0530 Subject: [PATCH 34/39] fix: Removing 'No Lab Tests created' message (cherry picked from commit 5e88e4e42255d71e815efb9898368e08472d22e2) --- healthcare/healthcare/doctype/lab_test/lab_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/healthcare/healthcare/doctype/lab_test/lab_test.py b/healthcare/healthcare/doctype/lab_test/lab_test.py index 8a06b102d2..aeb160fb68 100644 --- a/healthcare/healthcare/doctype/lab_test/lab_test.py +++ b/healthcare/healthcare/doctype/lab_test/lab_test.py @@ -127,8 +127,6 @@ def create_multiple(doctype, docname): frappe.msgprint( _("Lab Test(s) {0} created successfully").format(lab_test_created), indicator="green" ) - else: - frappe.msgprint(_("No Lab Tests created")) def create_lab_test_from_encounter(encounter): From 22f54a9da2c0aa9cfd295d254a831eb8eda6cea9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 1 Nov 2023 18:08:23 +0530 Subject: [PATCH 35/39] fix: linking existing customer to new Patient overwrites Customer Name (backport #306) (#309) * fix: linking existing customer to new Paitient overwrites customer name and other details (#306) test for the same (cherry picked from commit c5d9de2c78910c97b950299205f259e7b939bb67) Co-authored-by: Anoop --- .../healthcare/doctype/patient/patient.py | 51 ++++++++++++++----- .../doctype/patient/test_patient.py | 16 ++++++ 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/healthcare/healthcare/doctype/patient/patient.py b/healthcare/healthcare/doctype/patient/patient.py index 9f13b1ee54..3b70172503 100644 --- a/healthcare/healthcare/doctype/patient/patient.py +++ b/healthcare/healthcare/doctype/patient/patient.py @@ -32,6 +32,7 @@ def onload(self): def validate(self): self.set_full_name() self.flags.is_new_doc = self.is_new() + self.flags.existing_customer = self.is_new() and bool(self.customer) def before_insert(self): self.set_missing_customer_details() @@ -46,18 +47,13 @@ def after_insert(self): def on_update(self): if frappe.db.get_single_value("Healthcare Settings", "link_customer_to_patient"): if self.customer: - customer = frappe.get_doc("Customer", self.customer) - if self.customer_group: - customer.customer_group = self.customer_group - if self.territory: - customer.territory = self.territory - customer.customer_name = self.patient_name - customer.default_price_list = self.default_price_list - customer.default_currency = self.default_currency - customer.language = self.language - customer.image = self.image - customer.ignore_mandatory = True - customer.save(ignore_permissions=True) + if self.flags.existing_customer or frappe.db.exists( + {"doctype": "Patient", "name": ["!=", self.name], "customer": self.customer} + ): + self.update_patient_based_on_existing_customer() + else: + self.update_linked_customer() + else: create_customer(self) @@ -253,6 +249,35 @@ def update_contact(self, contact): contact.flags.skip_patient_update = True contact.save(ignore_permissions=True) + def update_linked_customer(self): + customer = frappe.get_doc("Customer", self.customer) + if self.customer_group: + customer.customer_group = self.customer_group + if self.territory: + customer.territory = self.territory + customer.customer_name = self.patient_name + customer.default_price_list = self.default_price_list + customer.default_currency = self.default_currency + customer.language = self.language + customer.image = self.image + customer.ignore_mandatory = True + customer.save(ignore_permissions=True) + + frappe.msgprint(_("Customer {0} updated").format(customer.name), alert=True) + + def update_patient_based_on_existing_customer(self): + customer = frappe.get_doc("Customer", self.customer) + self.db_set( + { + "customer_group": customer.customer_group, + "territory": customer.territory, + "default_price_list": customer.default_price_list, + "default_currency": customer.default_currency, + "language": customer.language, + } + ) + self.notify_update() + def create_customer(doc): customer = frappe.get_doc( @@ -271,7 +296,7 @@ def create_customer(doc): ).insert(ignore_permissions=True, ignore_mandatory=True) frappe.db.set_value("Patient", doc.name, "customer", customer.name) - frappe.msgprint(_("Customer {0} is created.").format(customer.name), alert=True) + frappe.msgprint(_("Customer {0} created and linked to Patient").format(customer.name), alert=True) def make_invoice(patient, company): diff --git a/healthcare/healthcare/doctype/patient/test_patient.py b/healthcare/healthcare/doctype/patient/test_patient.py index cd2feae5b5..79443f25b5 100644 --- a/healthcare/healthcare/doctype/patient/test_patient.py +++ b/healthcare/healthcare/doctype/patient/test_patient.py @@ -115,3 +115,19 @@ def test_patient_image_update_should_update_customer_image(self): customer = frappe.get_doc("Customer", patient.customer) self.assertEqual(customer.image, patient.image) + + def test_multiple_paients_linked_with_same_customer(self): + frappe.db.sql("""delete from `tabPatient`""") + frappe.db.set_single_value("Healthcare Settings", "link_customer_to_patient", 1) + + patient_name_1 = create_patient(patient_name="John Doe") + p1_customer_name = frappe.get_value("Patient", patient_name_1, "customer") + p1_customer = frappe.get_doc("Customer", p1_customer_name) + self.assertEqual(p1_customer.customer_name, "John Doe") + + patient_name_2 = create_patient(patient_name="Jane Doe", customer=p1_customer.name) + p2_customer_name = frappe.get_value("Patient", patient_name_2, "customer") + p2_customer = frappe.get_doc("Customer", p2_customer_name) + + self.assertEqual(p1_customer_name, p2_customer_name) + self.assertEqual(p2_customer.customer_name, "John Doe") From 07cdf410de38cef18cc9bb9e603d0d9a730970a2 Mon Sep 17 00:00:00 2001 From: Mohammed Irfan Date: Tue, 17 Oct 2023 15:56:56 +0530 Subject: [PATCH 36/39] fix: Add Discount Amount field to show payment dialog (cherry picked from commit 6d28ca54031b821ea5e18ff16503b45378e2c4f6) --- .../patient_appointment.js | 40 +++++++++++++------ .../patient_appointment.py | 12 ++++-- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js index 4a9acd311b..c2842005f1 100644 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js @@ -885,6 +885,12 @@ let make_payment = function (frm, automate_invoicing) { fieldname: "consultation_charge", fieldtype: "Currency", read_only: true, + }, + { + label: "Discount Amount", + fieldname: "discount_amount", + fieldtype: "Currency", + default: 0, } ]; @@ -925,21 +931,29 @@ let make_payment = function (frm, automate_invoicing) { fields: fields, primary_action_label: "Create Invoice", primary_action(values) { - frm.set_value("mode_of_payment", values.mode_of_payment) - frm.save(); - frappe.call({ - method: "healthcare.healthcare.doctype.patient_appointment.patient_appointment.invoice_appointment", - args: { "appointment_name": frm.doc.name }, - callback: async function (data) { - if (!data.exc) { - await frm.reload_doc(); - if (frm.doc.ref_sales_invoice) { - d.get_primary_btn().attr("disabled", true); - d.get_secondary_btn().attr("disabled", false); + if (values.consultation_charge >= values.discount_amount) { + frappe.call({ + method: "healthcare.healthcare.doctype.patient_appointment.patient_appointment.invoice_appointment", + args: { + "appointment_name": frm.doc.name, + "mode_of_payment": values.mode_of_payment, + "discount_amount": values.discount_amount + }, + callback: async function (data) { + if (!data.exc) { + await frm.reload_doc(); + if (frm.doc.ref_sales_invoice) { + d.get_field("mode_of_payment").$input.prop("disabled", true); + d.get_field("discount_amount").$input.prop("disabled", true); + d.get_primary_btn().attr("disabled", true); + d.get_secondary_btn().attr("disabled", false); + } } } - } - }); + }); + } else { + frappe.throw(__("Discount Amount should be less than or equal to Consultation Charge")) + } }, secondary_action_label: __(` diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py index c0db56ad42..4b32b03fea 100755 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py @@ -393,8 +393,9 @@ def check_payment_reqd(patient): @frappe.whitelist() -def invoice_appointment(appointment_name): +def invoice_appointment(appointment_name, mode_of_payment, discount_amount): appointment_doc = frappe.get_doc("Patient Appointment", appointment_name) + appointment_doc.mode_of_payment = mode_of_payment show_payment_popup = frappe.db.get_single_value("Healthcare Settings", "show_payment_popup") free_follow_ups = frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") @@ -410,11 +411,11 @@ def invoice_appointment(appointment_name): fee_validity = None if show_payment_popup and not appointment_doc.invoiced and not fee_validity: - create_sales_invoice(appointment_doc) + create_sales_invoice(appointment_doc, discount_amount) update_fee_validity(appointment_doc) -def create_sales_invoice(appointment_doc): +def create_sales_invoice(appointment_doc, discount_amount): sales_invoice = frappe.new_doc("Sales Invoice") sales_invoice.patient = appointment_doc.patient sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer") @@ -425,6 +426,7 @@ def create_sales_invoice(appointment_doc): item = sales_invoice.append("items", {}) item = get_appointment_item(appointment_doc, item) + appointment_doc.paid_amount = flt(item.amount) - flt(discount_amount) # Add payments if payment details are supplied else proceed to create invoice as Unpaid if appointment_doc.mode_of_payment and appointment_doc.paid_amount: @@ -433,6 +435,9 @@ def create_sales_invoice(appointment_doc): payment.mode_of_payment = appointment_doc.mode_of_payment payment.amount = appointment_doc.paid_amount + # Set discount amount in invoice equal to discount amount entered in payment popup + sales_invoice.discount_amount = flt(discount_amount) + sales_invoice.set_missing_values(for_validate=True) sales_invoice.flags.ignore_mandatory = True sales_invoice.save(ignore_permissions=True) @@ -445,6 +450,7 @@ def create_sales_invoice(appointment_doc): "invoiced": 1, "ref_sales_invoice": sales_invoice.name, "paid_amount": appointment_doc.paid_amount, + "mode_of_payment": appointment_doc.mode_of_payment, }, ) appointment_doc.reload() From f93d7cdec1219a8a4632041af7617482da86b2ac Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Sat, 28 Oct 2023 23:50:18 +0530 Subject: [PATCH 37/39] fix: Patient Appointment - Default fallback value in invoice_appointment and remove mode_of_payment argument (cherry picked from commit a18e3a44b7957e6ede29c33f408a4ab56f7df5d8) --- .../patient_appointment/patient_appointment.js | 9 +++++++-- .../patient_appointment/patient_appointment.py | 12 +++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js index c2842005f1..299510df00 100644 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js @@ -930,13 +930,13 @@ let make_payment = function (frm, automate_invoicing) { title: "Enter Payment Details", fields: fields, primary_action_label: "Create Invoice", - primary_action(values) { + primary_action: async function(values) { + await frm.save(); if (values.consultation_charge >= values.discount_amount) { frappe.call({ method: "healthcare.healthcare.doctype.patient_appointment.patient_appointment.invoice_appointment", args: { "appointment_name": frm.doc.name, - "mode_of_payment": values.mode_of_payment, "discount_amount": values.discount_amount }, callback: async function (data) { @@ -963,6 +963,11 @@ let make_payment = function (frm, automate_invoicing) { d.hide(); } }); + d.fields_dict["mode_of_payment"].df.onchange = () => { + if (d.get_value("mode_of_payment")) { + frm.set_value("mode_of_payment", d.get_value("mode_of_payment")); + } + }; d.get_secondary_btn().attr("disabled", true); d.set_values({ "patient": frm.doc.patient_name, diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py index 4b32b03fea..6cd4e79d6d 100755 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py @@ -393,9 +393,8 @@ def check_payment_reqd(patient): @frappe.whitelist() -def invoice_appointment(appointment_name, mode_of_payment, discount_amount): +def invoice_appointment(appointment_name, discount_amount=0): appointment_doc = frappe.get_doc("Patient Appointment", appointment_name) - appointment_doc.mode_of_payment = mode_of_payment show_payment_popup = frappe.db.get_single_value("Healthcare Settings", "show_payment_popup") free_follow_ups = frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") @@ -415,7 +414,7 @@ def invoice_appointment(appointment_name, mode_of_payment, discount_amount): update_fee_validity(appointment_doc) -def create_sales_invoice(appointment_doc, discount_amount): +def create_sales_invoice(appointment_doc, discount_amount=0): sales_invoice = frappe.new_doc("Sales Invoice") sales_invoice.patient = appointment_doc.patient sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer") @@ -426,14 +425,14 @@ def create_sales_invoice(appointment_doc, discount_amount): item = sales_invoice.append("items", {}) item = get_appointment_item(appointment_doc, item) - appointment_doc.paid_amount = flt(item.amount) - flt(discount_amount) + paid_amount = flt(appointment_doc.paid_amount) - flt(discount_amount) # Add payments if payment details are supplied else proceed to create invoice as Unpaid if appointment_doc.mode_of_payment and appointment_doc.paid_amount: sales_invoice.is_pos = 1 payment = sales_invoice.append("payments", {}) payment.mode_of_payment = appointment_doc.mode_of_payment - payment.amount = appointment_doc.paid_amount + payment.amount = paid_amount # Set discount amount in invoice equal to discount amount entered in payment popup sales_invoice.discount_amount = flt(discount_amount) @@ -449,8 +448,7 @@ def create_sales_invoice(appointment_doc, discount_amount): { "invoiced": 1, "ref_sales_invoice": sales_invoice.name, - "paid_amount": appointment_doc.paid_amount, - "mode_of_payment": appointment_doc.mode_of_payment, + "paid_amount": paid_amount }, ) appointment_doc.reload() From 920b7036740340ec967677e4fa11699b5dbdb8de Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Wed, 1 Nov 2023 17:07:56 +0530 Subject: [PATCH 38/39] fix: Patient Appointment - Add discount percentage and amount in payment popup (cherry picked from commit 0d7f6e27e0d87713a9973d7f49ebbfcf52c343a0) --- .../patient_appointment.js | 123 ++++++++++++++---- .../patient_appointment.py | 32 +++-- healthcare/healthcare/utils.py | 17 +++ 3 files changed, 135 insertions(+), 37 deletions(-) diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js index 299510df00..335faf25bb 100644 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.js @@ -880,12 +880,35 @@ let make_payment = function (frm, automate_invoicing) { options: "Mode of Payment", reqd: 1, }, + { + fieldtype: "Column Break", + }, { label: "Consultation Charge", fieldname: "consultation_charge", fieldtype: "Currency", read_only: true, }, + { + label: "Total Payable", + fieldname: "total_payable", + fieldtype: "Currency", + read_only: true, + }, + { + label: __("Additional Discount"), + fieldtype:"Section Break", + collapsible: 1, + }, + { + label: "Discount Percentage", + fieldname: "discount_percentage", + fieldtype: "Percent", + default: 0, + }, + { + fieldtype: "Column Break", + }, { label: "Discount Amount", fieldname: "discount_amount", @@ -901,7 +924,7 @@ let make_payment = function (frm, automate_invoicing) { fieldtype: "Data", read_only: true, }; - fields.splice(1, 0, pract_dict); + fields.splice(3, 0, pract_dict); } else if (frm.doc.appointment_for == "Service Unit") { let su_dict = { label: "Service Unit", @@ -909,7 +932,7 @@ let make_payment = function (frm, automate_invoicing) { fieldtype: "Data", read_only: true, }; - fields.splice(1, 0, su_dict); + fields.splice(3, 0, su_dict); } else if (frm.doc.appointment_for == "Department") { let dept_dict = { label: "Department", @@ -917,7 +940,7 @@ let make_payment = function (frm, automate_invoicing) { fieldtype: "Data", read_only: true, }; - fields.splice(1, 0, dept_dict); + fields.splice(3, 0, dept_dict); } if (automate_invoicing) { @@ -931,29 +954,29 @@ let make_payment = function (frm, automate_invoicing) { fields: fields, primary_action_label: "Create Invoice", primary_action: async function(values) { - await frm.save(); - if (values.consultation_charge >= values.discount_amount) { - frappe.call({ - method: "healthcare.healthcare.doctype.patient_appointment.patient_appointment.invoice_appointment", - args: { - "appointment_name": frm.doc.name, - "discount_amount": values.discount_amount - }, - callback: async function (data) { - if (!data.exc) { - await frm.reload_doc(); - if (frm.doc.ref_sales_invoice) { - d.get_field("mode_of_payment").$input.prop("disabled", true); - d.get_field("discount_amount").$input.prop("disabled", true); - d.get_primary_btn().attr("disabled", true); - d.get_secondary_btn().attr("disabled", false); - } + if (frm.is_dirty()) { + await frm.save(); + } + frappe.call({ + method: "healthcare.healthcare.doctype.patient_appointment.patient_appointment.invoice_appointment", + args: { + "appointment_name": frm.doc.name, + "discount_percentage": values.discount_percentage, + "discount_amount": values.discount_amount + }, + callback: async function (data) { + if (!data.exc) { + await frm.reload_doc(); + if (frm.doc.ref_sales_invoice) { + d.get_field("mode_of_payment").$input.prop("disabled", true); + d.get_field("discount_percentage").$input.prop("disabled", true); + d.get_field("discount_amount").$input.prop("disabled", true); + d.get_primary_btn().attr("disabled", true); + d.get_secondary_btn().attr("disabled", false); } } - }); - } else { - frappe.throw(__("Discount Amount should be less than or equal to Consultation Charge")) - } + } + }); }, secondary_action_label: __(` @@ -972,6 +995,7 @@ let make_payment = function (frm, automate_invoicing) { d.set_values({ "patient": frm.doc.patient_name, "consultation_charge": frm.doc.paid_amount, + "total_payable": frm.doc.paid_amount, }); if (frm.doc.appointment_for == "Practitioner") { @@ -986,5 +1010,56 @@ let make_payment = function (frm, automate_invoicing) { d.set_value("mode_of_payment", frm.doc.mode_of_payment); } d.show(); + + d.fields_dict["discount_percentage"].df.onchange = () => validate_discount("discount_percentage"); + d.fields_dict["discount_amount"].df.onchange = () => validate_discount("discount_amount"); + + function validate_discount(field) { + let message = ""; + let discount_percentage = d.get_value("discount_percentage"); + let discount_amount = d.get_value("discount_amount"); + let consultation_charge = d.get_value("consultation_charge"); + + if (field === "discount_percentage") { + if (discount_percentage > 100 || discount_percentage < 0) { + d.get_primary_btn().attr("disabled", true); + message = "Invalid discount percentage"; + } else { + d.get_primary_btn().attr("disabled", false); + frm.via_discount_percentage = true; + if (discount_percentage && discount_amount) { + d.set_value("discount_amount", 0); + } + discount_amount = consultation_charge * (discount_percentage / 100); + + d.set_values({ + "discount_amount": discount_amount, + "total_payable": consultation_charge - discount_amount, + }).then(() => delete frm.via_discount_percentage); + } + } else if (field === "discount_amount") { + if (consultation_charge < discount_amount || discount_amount < 0) { + d.get_primary_btn().attr("disabled", true); + message = "Discount amount should not be more than Consultation Charge"; + } else { + d.get_primary_btn().attr("disabled", false); + if (!frm.via_discount_percentage) { + discount_percentage = (discount_amount / consultation_charge) * 100; + d.set_values({ + "discount_percentage": discount_percentage, + "total_payable": consultation_charge - discount_amount, + }); + } + } + } + show_message(d, message, field); + } } }; + +let show_message = function(d, message, field) { + var field = d.get_field(field); + field.df.description = `
${message}
` + field.refresh(); +}; diff --git a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py index 6cd4e79d6d..cbde6c0554 100755 --- a/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/patient_appointment.py @@ -393,12 +393,11 @@ def check_payment_reqd(patient): @frappe.whitelist() -def invoice_appointment(appointment_name, discount_amount=0): +def invoice_appointment(appointment_name, discount_percentage=0, discount_amount=0): appointment_doc = frappe.get_doc("Patient Appointment", appointment_name) - show_payment_popup = frappe.db.get_single_value("Healthcare Settings", "show_payment_popup") - free_follow_ups = frappe.db.get_single_value("Healthcare Settings", "enable_free_follow_ups") + settings = frappe.get_single("Healthcare Settings") - if free_follow_ups: + if settings.enable_free_follow_ups: fee_validity = check_fee_validity(appointment_doc) if fee_validity and fee_validity.status != "Active": @@ -409,12 +408,12 @@ def invoice_appointment(appointment_name, discount_amount=0): else: fee_validity = None - if show_payment_popup and not appointment_doc.invoiced and not fee_validity: - create_sales_invoice(appointment_doc, discount_amount) + if settings.show_payment_popup and not appointment_doc.invoiced and not fee_validity: + create_sales_invoice(appointment_doc, discount_percentage, discount_amount) update_fee_validity(appointment_doc) -def create_sales_invoice(appointment_doc, discount_amount=0): +def create_sales_invoice(appointment_doc, discount_percentage=0, discount_amount=0): sales_invoice = frappe.new_doc("Sales Invoice") sales_invoice.patient = appointment_doc.patient sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer") @@ -425,7 +424,17 @@ def create_sales_invoice(appointment_doc, discount_amount=0): item = sales_invoice.append("items", {}) item = get_appointment_item(appointment_doc, item) - paid_amount = flt(appointment_doc.paid_amount) - flt(discount_amount) + + paid_amount = flt(appointment_doc.paid_amount) + # Set discount amount and percentage if entered in payment popup + if flt(discount_percentage): + sales_invoice.additional_discount_percentage = flt(discount_percentage) + paid_amount = flt(appointment_doc.paid_amount) - ( + flt(appointment_doc.paid_amount) * (flt(discount_percentage) / 100) + ) + if flt(discount_amount): + sales_invoice.discount_amount = flt(discount_amount) + paid_amount = flt(appointment_doc.paid_amount) - flt(discount_amount) # Add payments if payment details are supplied else proceed to create invoice as Unpaid if appointment_doc.mode_of_payment and appointment_doc.paid_amount: @@ -434,9 +443,6 @@ def create_sales_invoice(appointment_doc, discount_amount=0): payment.mode_of_payment = appointment_doc.mode_of_payment payment.amount = paid_amount - # Set discount amount in invoice equal to discount amount entered in payment popup - sales_invoice.discount_amount = flt(discount_amount) - sales_invoice.set_missing_values(for_validate=True) sales_invoice.flags.ignore_mandatory = True sales_invoice.save(ignore_permissions=True) @@ -448,10 +454,10 @@ def create_sales_invoice(appointment_doc, discount_amount=0): { "invoiced": 1, "ref_sales_invoice": sales_invoice.name, - "paid_amount": paid_amount + "paid_amount": paid_amount, }, ) - appointment_doc.reload() + appointment_doc.notify_update() @frappe.whitelist() diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index 027eda9868..03547f42e4 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -520,6 +520,23 @@ def manage_invoice_submit_cancel(doc, method): if fee_validity: frappe.db.set_value("Fee Validity", fee_validity, "sales_invoice_ref", doc.name) + if method == "on_cancel": + if doc.items and (doc.additional_discount_percentage or doc.discount_amount): + for item in doc.items: + if ( + item.get("reference_dt") + and item.get("reference_dn") + and item.get("reference_dt") == "Patient Appointment" + ): + frappe.db.set_value( + item.get("reference_dt"), + item.get("reference_dn"), + { + "paid_amount": item.amount, + "ref_sales_invoice": None, + }, + ) + def set_invoiced(item, method, ref_invoice=None): invoiced = False From 8ab8b14a3f7e2aa2892722682bfe2851a516f806 Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Sat, 28 Oct 2023 23:50:46 +0530 Subject: [PATCH 39/39] test: Patient Appointment - Add test cases for discount amount and percentage (cherry picked from commit 0765ebd0b12ce16e8be56f13b651ae7060cbeb6b) --- .../test_patient_appointment.py | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py b/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py index 059546eabb..bf1c5bacf0 100644 --- a/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/healthcare/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -84,6 +84,56 @@ def test_auto_invoicing(self): frappe.db.get_value("Sales Invoice", sales_invoice_name, "paid_amount"), appointment.paid_amount ) + def test_auto_invoicing_with_discount_amount(self): + patient, practitioner = create_healthcare_docs() + frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 1) + appointment = create_appointment( + patient, practitioner, nowdate(), invoice=1, discount_amount=100 + ) + self.assertEqual(frappe.db.get_value("Patient Appointment", appointment.name, "invoiced"), 1) + sales_invoice_name = frappe.db.get_value( + "Sales Invoice Item", {"reference_dn": appointment.name}, "parent" + ) + self.assertTrue(sales_invoice_name) + self.assertEqual( + frappe.db.get_value("Sales Invoice", sales_invoice_name, "company"), + appointment.company, + ) + self.assertEqual( + frappe.db.get_value("Sales Invoice", sales_invoice_name, "patient"), + appointment.patient, + ) + self.assertEqual( + frappe.db.get_value("Sales Invoice", sales_invoice_name, "paid_amount"), + (appointment.paid_amount - 100), + ) + + def test_auto_invoicing_with_discount_percentage(self): + patient, practitioner = create_healthcare_docs() + frappe.db.set_single_value("Healthcare Settings", "enable_free_follow_ups", 0) + frappe.db.set_single_value("Healthcare Settings", "show_payment_popup", 1) + appointment = create_appointment( + patient, practitioner, nowdate(), invoice=1, discount_percentage=10 + ) + self.assertEqual(frappe.db.get_value("Patient Appointment", appointment.name, "invoiced"), 1) + sales_invoice_name = frappe.db.get_value( + "Sales Invoice Item", {"reference_dn": appointment.name}, "parent" + ) + self.assertTrue(sales_invoice_name) + self.assertEqual( + frappe.db.get_value("Sales Invoice", sales_invoice_name, "company"), + appointment.company, + ) + self.assertEqual( + frappe.db.get_value("Sales Invoice", sales_invoice_name, "patient"), + appointment.patient, + ) + self.assertEqual( + frappe.db.get_value("Sales Invoice", sales_invoice_name, "paid_amount"), + (appointment.paid_amount - (appointment.paid_amount * (10 / 100))), + ) + def test_auto_invoicing_based_on_practitioner_department(self): patient, practitioner = create_healthcare_docs() frappe.db.set_value( @@ -600,6 +650,8 @@ def create_appointment( department=None, appointment_based_on_check_in=None, appointment_time=None, + discount_percentage=0, + discount_amount=0, ): item = create_healthcare_service_items() frappe.db.set_single_value("Healthcare Settings", "inpatient_visit_charge_item", item) @@ -626,7 +678,7 @@ def create_appointment( if save: appointment.save(ignore_permissions=True) if invoice or frappe.db.get_single_value("Healthcare Settings", "show_payment_popup"): - invoice_appointment(appointment.name) + invoice_appointment(appointment.name, discount_percentage, discount_amount) return appointment