From 5a6a75e3438b8ed0d62c20fc11263560685f4900 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 00:01:25 +0000 Subject: [PATCH 01/18] chore(deps): bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e5551c7b4..62a62b8f4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,7 +137,7 @@ jobs: uses: actions/download-artifact@v4 - name: Upload coverage data - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true From c2562951735d58b11879cf588361cbd5b2a3538f Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Sun, 15 Dec 2024 16:08:45 +0530 Subject: [PATCH 02/18] chore: update issue templates --- .github/ISSUE_TEMPLATE/bug_report.yaml | 16 +++++++++------- .github/ISSUE_TEMPLATE/feature_request.yaml | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index e74b7d06f2..82ba06ed48 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -10,10 +10,10 @@ body: Welcome to Marley Health issue tracker! Before creating an issue, please consider the following: 1. This tracker should only be used to report bugs and request features / enhancements to Marley Health - - For questions and general support, checkout the [documentation](https://frappehealth.com/docs) or use the [forum](https://discuss.frappe.io/c/healthcare/58) to get inputs from the open source community. - - For documentation issues, propose edit on the [documentation site](https://frappehealth.com/docs) directly. - 2. When making a bug report, make sure you provide all required information. The easier it is for - maintainers to reproduce, the faster it'll be fixed. + - For questions and general support, checkout the [documentation](https://marley.frappe.cloud/docs) or use the [forum](https://discuss.frappe.io/c/healthcare/58) to search for solutions / get inputs from the open source community. + - For documentation issues, propose edit on the [documentation site](https://marley.frappe.cloud/docs) directly. + - You can also post in the Telegram group for Marley Health development work: https://t.me/frappehealth + 2. When making a bug report, make sure you provide all required information. The easier it is for maintainers to reproduce, the faster it'll be fixed. 3. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉 - type: textarea @@ -34,13 +34,15 @@ body: options: - Outpatient Module - Inpatient ADT - - Laboratory + - Diagnostic Module - Clinical Procedure - Rehab & Physiotherapy - Patient History + - Code Standards - Setup / Configuration - Integration with ERPNext modules - Regional / Other Integrations + - Laboratory (legacy) validations: required: true @@ -61,10 +63,10 @@ body: attributes: label: Installation method options: + - Frappe Cloud - docker - easy-install - manual install - - FrappeCloud validations: required: false @@ -80,7 +82,7 @@ body: attributes: label: Code of Conduct description: | - By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/health/blob/develop/CODE_OF_CONDUCT.md) + By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/earthains/marley/blob/develop/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index c7a34bb31c..0169d35e57 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -10,7 +10,7 @@ body: Welcome to Marley Health issue tracker! Before submitting a request, please consider the following: 1. This tracker should only be used to report bugs and request features / enhancements to Marley Health - - For questions and general support, checkout the [documentation](https://frappehealth.com/docs) or use the [forum](https://discuss.frappe.io/c/healthcare/58) to get inputs from the open source community. + - For questions and general support, checkout the [documentation](https://marley.frappe.cloud/docs) or use the [forum](https://discuss.frappe.io/c/healthcare/58) to get inputs from the open source community. 2. Use the search function before creating a new issue. Duplicates will be closed and directed to the original discussion. 3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen. @@ -20,9 +20,9 @@ body: If you're in urgent need to a feature, please try the following channels to get paid developments done quickly: 1. Certified Frappe partners: https://frappe.io/partners - 2. Healthcare Category on Frappe forum: https://discuss.frappe.io/c/healthcare/58 - 2. Developer community on Frappe forum: https://discuss.frappe.io/c/developers/5 - 3. Telegram group for Marley Health development work: https://t.me/frappehealth + 2. Telegram group for Marley Health development work: https://t.me/frappehealth + 3. Healthcare Category on Frappe forum: https://discuss.frappe.io/c/healthcare/58 + 4. Developer community on Frappe forum: https://discuss.frappe.io/c/developers/5 - type: textarea id: problem-info From d70be7886d02710c96e37c3dcf506fef5cd45415 Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Sun, 15 Dec 2024 16:11:29 +0530 Subject: [PATCH 03/18] chore: update code of conduct to v2.1 --- CODE_OF_CONDUCT.md | 119 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 98 insertions(+), 21 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 4a97d8bf97..b824ce3c06 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,45 +2,122 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@frappe.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +devs@earthianslive.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[FAQ]: https://www.contributor-covenant.org/faq \ No newline at end of file From 146a149d664216c2596a12b556942342fa967e59 Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Sun, 15 Dec 2024 12:35:34 +0530 Subject: [PATCH 04/18] chore: fix weekly release workflow config --- .github/workflows/initiate_release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml index da630f84fc..ad9060a094 100644 --- a/.github/workflows/initiate_release.yml +++ b/.github/workflows/initiate_release.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: jobs: - stable-release: + create-stable-release: name: Release runs-on: ubuntu-latest strategy: @@ -25,8 +25,8 @@ jobs: repo: marley title: |- "chore: release v${{ matrix.version }}" - body: "Automated release." + body: "Automated Release." base: version-${{ matrix.version }} head: version-${{ matrix.version }}-hotfix env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.EARTHIANS_BOT_TOKEN }} \ No newline at end of file From abf6a22ff32bfde53fbad70a539e3ec435e28541 Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Sun, 15 Dec 2024 00:44:08 +0530 Subject: [PATCH 05/18] chore: update mergify config --- .mergify.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index 21cdb74802..2450b166a1 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -3,9 +3,8 @@ pull_request_rules: conditions: - and: - author!=Sajinsr - - author!=akashkrishna619 - author!=akurungadam - - author!=frappe-pr-bot + - author!=earthians-pr-bot - author!=mergify[bot] - author!=github-actions[bot] - or: From d1f28b676b3000af333dee14cb0f5c3dbb0905c5 Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Sun, 15 Dec 2024 00:43:50 +0530 Subject: [PATCH 06/18] chore: fix symantic release config --- .releaserc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.releaserc b/.releaserc index a5489bdd2e..23c27e1ccd 100644 --- a/.releaserc +++ b/.releaserc @@ -1,5 +1,5 @@ { - "branches": ["version-14"], + "branches": ["version-14", "version-15"], "plugins": [ "@semantic-release/commit-analyzer", { "preset": "angular", From 7108dfb6b84e36955d93298aec2b7c342e758915 Mon Sep 17 00:00:00 2001 From: Anoop Kurungadam Date: Sun, 15 Dec 2024 12:37:18 +0530 Subject: [PATCH 07/18] chore: update semantic-release workflow config --- .github/workflows/on_release.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/on_release.yml b/.github/workflows/on_release.yml index cbe503d342..9d82bed5cf 100644 --- a/.github/workflows/on_release.yml +++ b/.github/workflows/on_release.yml @@ -4,6 +4,7 @@ on: push: branches: - version-14 + - version-15 jobs: release: name: Release @@ -23,10 +24,10 @@ jobs: npm install @semantic-release/git @semantic-release/exec --no-save - name: Create Release env: - GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} - GIT_AUTHOR_NAME: "Frappe PR Bot" - GIT_AUTHOR_EMAIL: "developers@frappe.io" - GIT_COMMITTER_NAME: "Frappe PR Bot" - GIT_COMMITTER_EMAIL: "developers@frappe.io" + GH_TOKEN: ${{ secrets.EARTHIANS_BOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.EARTHIANS_BOT_TOKEN }} + GIT_AUTHOR_NAME: "earthians PR Bot" + GIT_AUTHOR_EMAIL: "devs@earthianslive.com" + GIT_COMMITTER_NAME: "earthians PR Bot" + GIT_COMMITTER_EMAIL: "devs@earthianslive.com" run: npx semantic-release \ No newline at end of file From db44419f3a40ce98ccf0dfefe7aa14fe29c3e0a3 Mon Sep 17 00:00:00 2001 From: Anoop Date: Sun, 15 Dec 2024 22:48:01 +0530 Subject: [PATCH 08/18] chore: fix typo in bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 82ba06ed48..9c3d63bfa5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -82,7 +82,7 @@ body: attributes: label: Code of Conduct description: | - By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/earthains/marley/blob/develop/CODE_OF_CONDUCT.md) + By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/earthians/marley/blob/develop/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true From 83816a8ab9b50a0e40ae29e7a8a9b162e493a6ac Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Sat, 30 Nov 2024 16:14:23 +0530 Subject: [PATCH 09/18] feat: Healthcare Package --- .../custom_doctype/payment_entry.py | 28 +- .../doctype/healthcare_package/__init__.py | 0 .../healthcare_package/healthcare_package.js | 188 ++++++++++++ .../healthcare_package.json | 273 ++++++++++++++++++ .../healthcare_package/healthcare_package.py | 91 ++++++ .../test_healthcare_package.py | 9 + .../healthcare_package_item/__init__.py | 0 .../healthcare_package_item.json | 148 ++++++++++ .../healthcare_package_item.py | 9 + .../doctype/package_subscription/__init__.py | 0 .../package_subscription.js | 51 ++++ .../package_subscription.json | 268 +++++++++++++++++ .../package_subscription.py | 84 ++++++ .../package_subscription_list.js | 15 + .../test_package_subscription.py | 9 + healthcare/healthcare/utils.py | 41 +++ healthcare/hooks.py | 4 +- healthcare/patches.txt | 1 + ...e_custom_field_for_package_subscription.py | 18 ++ healthcare/setup.py | 8 + 20 files changed, 1231 insertions(+), 14 deletions(-) create mode 100644 healthcare/healthcare/doctype/healthcare_package/__init__.py create mode 100644 healthcare/healthcare/doctype/healthcare_package/healthcare_package.js create mode 100644 healthcare/healthcare/doctype/healthcare_package/healthcare_package.json create mode 100644 healthcare/healthcare/doctype/healthcare_package/healthcare_package.py create mode 100644 healthcare/healthcare/doctype/healthcare_package/test_healthcare_package.py create mode 100644 healthcare/healthcare/doctype/healthcare_package_item/__init__.py create mode 100644 healthcare/healthcare/doctype/healthcare_package_item/healthcare_package_item.json create mode 100644 healthcare/healthcare/doctype/healthcare_package_item/healthcare_package_item.py create mode 100644 healthcare/healthcare/doctype/package_subscription/__init__.py create mode 100644 healthcare/healthcare/doctype/package_subscription/package_subscription.js create mode 100644 healthcare/healthcare/doctype/package_subscription/package_subscription.json create mode 100644 healthcare/healthcare/doctype/package_subscription/package_subscription.py create mode 100644 healthcare/healthcare/doctype/package_subscription/package_subscription_list.js create mode 100644 healthcare/healthcare/doctype/package_subscription/test_package_subscription.py create mode 100644 healthcare/patches/v15_0/create_custom_field_for_package_subscription.py diff --git a/healthcare/healthcare/custom_doctype/payment_entry.py b/healthcare/healthcare/custom_doctype/payment_entry.py index 6a6fd53d9d..ddb732e8bf 100644 --- a/healthcare/healthcare/custom_doctype/payment_entry.py +++ b/healthcare/healthcare/custom_doctype/payment_entry.py @@ -2,21 +2,25 @@ @frappe.whitelist() -def set_paid_amount_in_treatment_counselling(doc, method): - if doc.treatment_counselling and doc.paid_amount: +def set_paid_amount_in_healthcare_docs(doc, method): + if doc.paid_amount: on_cancel = True if method == "on_cancel" else False - validate_treatment_counselling(doc, on_cancel) + if doc.treatment_counselling: + validate_doc("Treatment Counselling", doc.treatment_counselling, doc.paid_amount, on_cancel) + if doc.package_subscription: + validate_doc("Package Subscription", doc.package_subscription, doc.paid_amount, on_cancel) -def validate_treatment_counselling(doc, on_cancel=False): - treatment_counselling_doc = frappe.get_doc("Treatment Counselling", doc.treatment_counselling) +def validate_doc(doctype, docname, paid_amount, on_cancel=False): + doc = frappe.get_doc(doctype, docname) - paid_amount = treatment_counselling_doc.paid_amount + doc.paid_amount if on_cancel: - paid_amount = treatment_counselling_doc.paid_amount - doc.paid_amount + paid_amount = doc.paid_amount - paid_amount + else: + paid_amount = doc.paid_amount + paid_amount - treatment_counselling_doc.paid_amount = paid_amount - treatment_counselling_doc.outstanding_amount = ( - treatment_counselling_doc.amount - treatment_counselling_doc.paid_amount - ) - treatment_counselling_doc.save() + doc.paid_amount = paid_amount + amount = doc.total_package_amount if doctype == "Package Subscription" else doc.amount + + doc.outstanding_amount = amount - doc.paid_amount + doc.save() diff --git a/healthcare/healthcare/doctype/healthcare_package/__init__.py b/healthcare/healthcare/doctype/healthcare_package/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/healthcare/healthcare/doctype/healthcare_package/healthcare_package.js b/healthcare/healthcare/doctype/healthcare_package/healthcare_package.js new file mode 100644 index 0000000000..05eb2c5de5 --- /dev/null +++ b/healthcare/healthcare/doctype/healthcare_package/healthcare_package.js @@ -0,0 +1,188 @@ +// Copyright (c) 2024, earthians Health Informatics Pvt. Ltd. and contributors +// For license information, please see license.txt + +let package_docs_filter = ["Item","Clinical Procedure Template", "Observation Template", "Therapy Type"] + +frappe.ui.form.on("Healthcare Package", { + refresh: function (frm) { + frm.set_query("price_list", function() { + return { + filters: { + "selling": 1, + } + }; + }); + + frm.set_query("package_item_type", "package_items", function() { + return { + filters: { + "name": ["in", package_docs_filter], + } + }; + }); + + frm.set_query("income_account", function() { + return { + filters: { + "account_type": "Income Account", + "is_group": 0 + } + }; + }); + }, + + onload: function (frm) { + if (frm.doc.is_billable) { + let read_only = frm.doc.item ? 1 : 0; + frm.set_df_property("item_code", "read_only", read_only); + frm.set_df_property("item_group", "read_only", read_only); + } + }, + + total_amount: function (frm) { + calculate_total_payable(frm); + }, + + apply_discount_on: function (frm) { + if(frm.doc.discount_percentage) { + frm.trigger("discount_percentage"); + } + }, + + discount_percentage: function (frm) { + frm.via_discount_percentage = true; + + if(frm.doc.discount_percentage && frm.doc.discount_amount) { + frm.doc.discount_amount = 0; + } + + let discount_field = frm.doc.apply_discount_on == "Total" ? "total_amount" : "net_total"; + var total = flt(frm.doc[discount_field]); + + var discount_amount = flt(total * flt(frm.doc.discount_percentage) / 100, + precision("discount_amount")); + + frm.set_value("discount_amount", discount_amount) + .then(() => delete frm.via_discount_percentage); + calculate_total_payable(frm); + }, + + discount_amount: function (frm) { + if (!frm.via_discount_percentage) { + frm.doc.additional_discount_percentage = 0; + let discount_field = frm.doc.apply_discount_on == "Total" ? "total_amount" : "net_total"; + var total = flt(frm.doc[discount_field]); + var discount_percentage = (flt(frm.doc.discount_amount) / total) * 100; + + frm.set_value("discount_percentage", discount_percentage) + calculate_total_payable(frm); + } + } +}); + +frappe.ui.form.on("Healthcare Package Item", { + package_item: async function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (frm.doc.price_list && row.package_item_type && row.package_item) { + if (row.package_item_type == "Item") { + frappe.model.set_value(cdt, cdn, "item_code", row.package_item) + set_item_rate(frm, row, row.package_item); + } else { + let item = (await frappe.db.get_value(row.package_item_type, row.package_item, "item")).message.item; + if (item) { + console.log() + frappe.model.set_value(cdt, cdn, "item_code", item) + set_item_rate(frm, row, item); + } + } + } + }, + + rate: function (frm, cdt, cdn) { + set_amount(frm, cdt, cdn); + }, + + no_of_sessions: function (frm, cdt, cdn) { + set_amount(frm, cdt, cdn); + }, + + amount: function (frm, cdt, cdn) { + let row = locals[cdt][cdn]; + set_child_discounted_amount(row); + }, + + amount_with_discount: function (frm, cdt, cdn) { + set_totals(frm); + }, + + discount_percentage: function (frm, cdt, cdn) { + let row = locals[cdt][cdn]; + frm.via_child_discount_percentage = true; + + if(row.discount_percentage && row.discount_amount) { + row.discount_amount = 0; + } + + var discount_amount = flt(row.amount * flt(row.discount_percentage) / 100, + precision("discount_amount")); + + frappe.model.set_value(cdt, cdn, "discount_amount", discount_amount) + .then(() => delete frm.via_child_discount_percentage); + set_child_discounted_amount(row); + }, + + discount_amount: function (frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (!frm.via_child_discount_percentage) { + row.discount_percentage = 0; + var discount_percentage = (flt(row.discount_amount) / row.amount) * 100; + + frappe.model.set_value(cdt, cdn, "discount_percentage", discount_percentage) + set_child_discounted_amount(row); + } + } +}); + +var set_totals = function (frm) { + let total = 0; + let net_total = 0; + frm.doc.package_items.forEach((item) => { + total += item.amount_with_discount; + net_total += item.amount; + }); + frm.set_value("total_amount", total); + frm.set_value("net_total", net_total); +}; + +var set_amount = function (frm, cdt, cdn) { + row = locals[cdt][cdn]; + if (row.rate && row.no_of_sessions) { + frappe.model.set_value(cdt, cdn, "amount", (row.rate * row.no_of_sessions) - row.discount_amount); + } +}; + +var calculate_total_payable = function (frm) { + if (frm.doc.discount_amount) { + let discount_field = frm.doc.apply_discount_on == "Total" ? "total_amount" : "net_total"; + var total = flt(frm.doc[discount_field]); + frm.set_value("discount_amount", flt(total * flt(frm.doc.discount_percentage) / 100)); + frm.set_value("total_package_amount", total - flt(total * flt(frm.doc.discount_percentage) / 100)); + } else { + frm.set_value("total_package_amount", frm.doc.total_amount); + } +}; + +var set_child_discounted_amount = function (row) { + frappe.model.set_value(row.doctype, row.name, "amount_with_discount", row.amount - row.discount_amount) +}; + +var set_item_rate = function (frm, row, item) { + frappe.db.get_value("Item Price", { + "item_code": item, + "price_list": frm.doc.price_list + }, "price_list_rate") + .then(r => { + let price_list_rate = r.message.price_list_rate ? r.message.price_list_rate : 0; + frappe.model.set_value(row.doctype, row.name, "rate", price_list_rate); + }); +}; diff --git a/healthcare/healthcare/doctype/healthcare_package/healthcare_package.json b/healthcare/healthcare/doctype/healthcare_package/healthcare_package.json new file mode 100644 index 0000000000..18bf3fc19c --- /dev/null +++ b/healthcare/healthcare/doctype/healthcare_package/healthcare_package.json @@ -0,0 +1,273 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:package_name", + "creation": "2024-10-15 16:08:09.686811", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "section_break_mp9z", + "disabled", + "section_break_lgqb", + "package_name", + "currency_and_price_list_section", + "price_list", + "column_break_samq", + "currency", + "section_break_cydn", + "income_account", + "section_break_ascf", + "package_items", + "section_break_zmmd", + "net_total", + "column_break_axok", + "total_amount", + "discount_details_section", + "apply_discount_on", + "discount_percentage", + "column_break_zgdp", + "discount_amount", + "total_section", + "column_break_gdsk", + "column_break_oqhd", + "total_package_amount", + "billing_tab", + "item", + "item_code", + "item_group", + "column_break_gcaf", + "item_wise_invoicing" + ], + "fields": [ + { + "fieldname": "section_break_mp9z", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "section_break_lgqb", + "fieldtype": "Section Break" + }, + { + "fieldname": "package_name", + "fieldtype": "Data", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Package Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "price_list", + "fieldtype": "Link", + "label": "Price List", + "options": "Price List" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "section_break_ascf", + "fieldtype": "Section Break" + }, + { + "fieldname": "package_items", + "fieldtype": "Table", + "label": "Package Items", + "options": "Healthcare Package Item" + }, + { + "fieldname": "section_break_zmmd", + "fieldtype": "Section Break" + }, + { + "fieldname": "total_package_amount", + "fieldtype": "Currency", + "label": "Total Package Amount", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "column_break_axok", + "fieldtype": "Column Break" + }, + { + "fieldname": "total_amount", + "fieldtype": "Currency", + "label": "Total Amount", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "column_break_zgdp", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: doc.discount_percentage || doc.discount_amount", + "fieldname": "discount_details_section", + "fieldtype": "Section Break", + "label": "Discount Details" + }, + { + "fieldname": "discount_percentage", + "fieldtype": "Percent", + "label": "Discount Percentage" + }, + { + "fieldname": "discount_amount", + "fieldtype": "Currency", + "label": "Discount Amount", + "options": "currency" + }, + { + "fieldname": "currency_and_price_list_section", + "fieldtype": "Section Break", + "label": "Currency and Price List" + }, + { + "fieldname": "column_break_samq", + "fieldtype": "Column Break" + }, + { + "fieldname": "billing_tab", + "fieldtype": "Tab Break", + "label": "Billing" + }, + { + "fieldname": "item", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Item", + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "item_code", + "fieldtype": "Data", + "label": "Item Code", + "reqd": 1 + }, + { + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group", + "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_cydn", + "fieldtype": "Section Break", + "label": "Accounts" + }, + { + "fieldname": "income_account", + "fieldtype": "Link", + "label": "Income Account", + "options": "Account" + }, + { + "fieldname": "column_break_gcaf", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If checked, billing will happen against package items", + "fieldname": "item_wise_invoicing", + "fieldtype": "Check", + "label": "Item wise Invoicing" + }, + { + "depends_on": "eval: doc.discount_percentage || doc.discount_amount", + "fieldname": "net_total", + "fieldtype": "Currency", + "label": "Net Total", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "total_section", + "fieldtype": "Section Break", + "label": "Total" + }, + { + "fieldname": "column_break_gdsk", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_oqhd", + "fieldtype": "Column Break" + }, + { + "default": "Total", + "fieldname": "apply_discount_on", + "fieldtype": "Select", + "label": "Apply Discount on", + "options": "Total\nNet Total" + } + ], + "index_web_pages_for_search": 1, + "links": [ + { + "link_doctype": "Package Subscription", + "link_fieldname": "healthcare_package" + } + ], + "modified": "2024-10-28 18:29:22.391899", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Healthcare Package", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Healthcare Administrator", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Physician", + "share": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "total_amount", + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/healthcare/healthcare/doctype/healthcare_package/healthcare_package.py b/healthcare/healthcare/doctype/healthcare_package/healthcare_package.py new file mode 100644 index 0000000000..3e59c8030f --- /dev/null +++ b/healthcare/healthcare/doctype/healthcare_package/healthcare_package.py @@ -0,0 +1,91 @@ +# Copyright (c) 2024, earthians Health Informatics Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.utils import flt, today + + +class HealthcarePackage(Document): + def validate(self): + self.calculate_totals() + self.create_item_from_package() + + def calculate_totals(self): + total, net_total = 0, 0 + + for item in self.package_items: + total += item.amount_with_discount + net_total += item.amount + + self.total_amount = total + self.net_total = net_total + + if self.discount_amount: + self.discount_percentage = (flt(self.discount_amount) / self.total_amount) * 100 + self.total_package_amount = self.total_amount - self.discount_amount + elif self.discount_percentage: + self.discount_amount = flt(self.total_amount * flt(self.discount_percentage) / 100) + self.total_package_amount = self.total_amount - self.discount_amount + else: + self.total_package_amount = self.total_amount + + def create_item_from_package(self): + item_name = self.item + if not item_name: + uom = frappe.db.exists("UOM", "Nos") or frappe.db.get_single_value( + "Stock Settings", "stock_uom" + ) + + item_exists = frappe.db.exists("Item", self.item_code) + if not item_exists: + # Insert item + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": self.item_code, + "item_name": self.package_name, + "item_group": self.item_group, + "description": self.package_name, + "is_sales_item": 1, + "is_service_item": 1, + "is_purchase_item": 0, + "is_stock_item": 0, + "include_item_in_manufacturing": 0, + "show_in_website": 0, + "is_pro_applicable": 0, + "disabled": 0, + "stock_uom": uom, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) + + item_name = item.name + else: + item_name = item_exists + + # Set item in the template + self.item = item_name + + self.make_item_price(item_name, self.total_package_amount) + + def make_item_price(self, item, item_price=0.0): + exists = frappe.db.exists( + "Item Price", + { + "price_list": self.price_list, + "item_code": item, + "valid_from": today(), + }, + ) + if not exists: + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": self.price_list, + "item_code": item, + "price_list_rate": item_price, + "valid_from": today(), + } + ).insert(ignore_permissions=True, ignore_mandatory=True) + else: + frappe.db.set_value("Item Price", exists, "price_list_rate", item_price) diff --git a/healthcare/healthcare/doctype/healthcare_package/test_healthcare_package.py b/healthcare/healthcare/doctype/healthcare_package/test_healthcare_package.py new file mode 100644 index 0000000000..7c97ac25bb --- /dev/null +++ b/healthcare/healthcare/doctype/healthcare_package/test_healthcare_package.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, earthians Health Informatics Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestHealthcarePackage(FrappeTestCase): + pass diff --git a/healthcare/healthcare/doctype/healthcare_package_item/__init__.py b/healthcare/healthcare/doctype/healthcare_package_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/healthcare/healthcare/doctype/healthcare_package_item/healthcare_package_item.json b/healthcare/healthcare/doctype/healthcare_package_item/healthcare_package_item.json new file mode 100644 index 0000000000..0259999670 --- /dev/null +++ b/healthcare/healthcare/doctype/healthcare_package_item/healthcare_package_item.json @@ -0,0 +1,148 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-10-16 11:14:49.649656", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "package_item_type", + "package_item", + "item_code", + "column_break_dyhx", + "no_of_sessions", + "booked_sessions", + "invoiced", + "section_break_bbtg", + "rate", + "column_break_doch", + "amount", + "amount_with_discount", + "discount_details_section", + "discount_percentage", + "column_break_nnxm", + "discount_amount" + ], + "fields": [ + { + "fieldname": "package_item_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Package Item Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "package_item", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Package Item", + "options": "package_item_type", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "read_only": 1, + "reqd": 1 + }, + { + "default": "1", + "fieldname": "no_of_sessions", + "fieldtype": "Float", + "in_list_view": 1, + "label": "No of Sessions", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "discount_percentage", + "fieldtype": "Percent", + "label": "Discount Percentage" + }, + { + "default": "0", + "fieldname": "discount_amount", + "fieldtype": "Currency", + "label": "Discount Amount" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: doc.discount_percentage || doc.discount_amount", + "depends_on": "eval: doc.parenttype == \"Healthcare Package\"", + "fieldname": "discount_details_section", + "fieldtype": "Section Break", + "label": "Discount Details" + }, + { + "fieldname": "column_break_nnxm", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_dyhx", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_bbtg", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_doch", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "amount_with_discount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount (With Discount)", + "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval: doc.parenttype == \"Package Subscription\"", + "fieldname": "booked_sessions", + "fieldtype": "Float", + "label": "Booked Sessions", + "read_only": 1 + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "label": "Item Code", + "options": "Item", + "read_only_depends_on": "eval: doc.package_item_type != \"Medication\"" + }, + { + "default": "0", + "depends_on": "eval: doc.parenttype == \"Package Subscription\"", + "fieldname": "invoiced", + "fieldtype": "Check", + "label": "Invoiced", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-12-01 10:42:54.758370", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Healthcare Package Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/healthcare/healthcare/doctype/healthcare_package_item/healthcare_package_item.py b/healthcare/healthcare/doctype/healthcare_package_item/healthcare_package_item.py new file mode 100644 index 0000000000..2de89a2e1d --- /dev/null +++ b/healthcare/healthcare/doctype/healthcare_package_item/healthcare_package_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, earthians Health Informatics Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class HealthcarePackageItem(Document): + pass diff --git a/healthcare/healthcare/doctype/package_subscription/__init__.py b/healthcare/healthcare/doctype/package_subscription/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/healthcare/healthcare/doctype/package_subscription/package_subscription.js b/healthcare/healthcare/doctype/package_subscription/package_subscription.js new file mode 100644 index 0000000000..5172033ba1 --- /dev/null +++ b/healthcare/healthcare/doctype/package_subscription/package_subscription.js @@ -0,0 +1,51 @@ +// Copyright (c) 2024, earthians Health Informatics Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Package Subscription", { + setup: function (frm) { + frm.set_query("patient", function () { + return { + filters: { + status: "Active", + }, + }; + }); + frm.set_query("healthcare_package", function () { + return { + filters: { + disabled: 0, + }, + }; + }); + }, + + refresh: function (frm) { + if (frm.doc.outstanding_amount > 0) { + frm.add_custom_button(__("Payment Entry"), function() { + frappe.call({ + method: "healthcare.healthcare.doctype.package_subscription.package_subscription.create_payment_entry", + args: { + package_subscription: frm.doc.name + }, + callback: function (r) { + if (r && r.message) { + frappe.set_route("Form", "Payment Entry", r.message); + } + } + }); + }, "Create") + } + }, + + healthcare_package: function (frm) { + if (frm.doc.healthcare_package) { + frappe.call({ + doc: frm.doc, + method: "get_package_details", + callback: function (r) { + frm.refresh(); + }, + }); + } + } +}); diff --git a/healthcare/healthcare/doctype/package_subscription/package_subscription.json b/healthcare/healthcare/doctype/package_subscription/package_subscription.json new file mode 100644 index 0000000000..87c676a048 --- /dev/null +++ b/healthcare/healthcare/doctype/package_subscription/package_subscription.json @@ -0,0 +1,268 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2024-10-18 14:54:33.862440", + "default_view": "List", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "section_break_w7l6", + "naming_series", + "patient", + "patient_name", + "healthcare_package", + "column_break_yboa", + "company", + "status", + "valid_to", + "amended_from", + "accounts_section", + "income_account", + "section_break_drdy", + "package_details", + "section_break_dapx", + "discount_amount", + "column_break_zpsu", + "total_amount", + "totals_section", + "column_break_yoac", + "column_break_eoaw", + "total_package_amount", + "outstanding_amount", + "paid_amount", + "section_break_bwrj", + "invoiced", + "item_wise_invoicing" + ], + "fields": [ + { + "fieldname": "section_break_w7l6", + "fieldtype": "Section Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Package Subscription", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "patient", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Patient", + "options": "Patient", + "reqd": 1 + }, + { + "fieldname": "healthcare_package", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Healthcare Package", + "options": "Healthcare Package", + "reqd": 1 + }, + { + "fieldname": "column_break_yboa", + "fieldtype": "Column Break" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "\nPaid\nPartially Paid\nUnpaid\nDiscontinued" + }, + { + "fieldname": "section_break_drdy", + "fieldtype": "Section Break" + }, + { + "fieldname": "package_details", + "fieldtype": "Table", + "label": "Package Details", + "options": "Healthcare Package Item", + "read_only": 1 + }, + { + "fieldname": "section_break_dapx", + "fieldtype": "Section Break" + }, + { + "fieldname": "total_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total Amount", + "read_only": 1 + }, + { + "fieldname": "column_break_zpsu", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "paid_amount", + "fieldtype": "Currency", + "label": "Paid Amount", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "outstanding_amount", + "fieldtype": "Currency", + "label": "Outstanding Amount", + "read_only": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "HLC-PSUB-.YYYY.-" + }, + { + "fieldname": "discount_amount", + "fieldtype": "Currency", + "label": "Discount Amount", + "read_only": 1 + }, + { + "fieldname": "total_package_amount", + "fieldtype": "Currency", + "label": "Total Package Amount", + "read_only": 1 + }, + { + "fetch_from": "patient.patient_name", + "fetch_if_empty": 1, + "fieldname": "patient_name", + "fieldtype": "Data", + "label": "Patient Name", + "read_only": 1 + }, + { + "fieldname": "accounts_section", + "fieldtype": "Section Break", + "label": "Accounts" + }, + { + "fieldname": "income_account", + "fieldtype": "Link", + "label": "Income Account", + "options": "Account" + }, + { + "fieldname": "totals_section", + "fieldtype": "Section Break", + "label": "Totals" + }, + { + "fieldname": "column_break_yoac", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_eoaw", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "invoiced", + "fieldtype": "Check", + "label": "Invoiced", + "read_only": 1 + }, + { + "fieldname": "section_break_bwrj", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fetch_from": "healthcare_package.item_wise_invoicing", + "fieldname": "item_wise_invoicing", + "fieldtype": "Check", + "hidden": 1, + "label": "Item wise Invoicing", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "valid_to", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Valid To", + "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [ + { + "link_doctype": "Payment Entry", + "link_fieldname": "package_subscription" + } + ], + "modified": "2024-12-01 10:12:36.922295", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Package Subscription", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Healthcare Administrator", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Physician", + "share": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "search_fields": "patient_name, total_amount", + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/healthcare/healthcare/doctype/package_subscription/package_subscription.py b/healthcare/healthcare/doctype/package_subscription/package_subscription.py new file mode 100644 index 0000000000..57d030145b --- /dev/null +++ b/healthcare/healthcare/doctype/package_subscription/package_subscription.py @@ -0,0 +1,84 @@ +# Copyright (c) 2024, earthians Health Informatics Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document, _ +from frappe.utils import get_link_to_form + + +class PackageSubscription(Document): + def validate(self): + if self.total_package_amount: + self.outstanding_amount = self.total_package_amount - self.paid_amount + + if not self.status == "Discontinued": + if self.paid_amount == 0: + self.status = "Unpaid" + else: + self.status = "Paid" if self.outstanding_amount == 0 else "Partially Paid" + + def on_update_after_submit(self): + if self.total_package_amount: + self.db_set("outstanding_amount", self.total_package_amount - self.paid_amount) + + if not self.status == "Discontinued": + if self.paid_amount == 0: + self.db_set("status", "Unpaid") + else: + self.db_set("status", "Paid" if self.outstanding_amount == 0 else "Partially Paid") + + def before_insert(self): + exists = frappe.db.exists( + "Package Subscription", + { + "patient": self.patient, + "healthcare_package": self.healthcare_package, + "valid_to": [">=", self.valid_to], + "docstatus": ["!=", 2], + }, + ) + + if exists: + frappe.throw( + _("Subscription already exists for patient {0}: {1}").format( + frappe.bold(self.patient_name), get_link_to_form("Package Subscription", exists) + ) + ) + + @frappe.whitelist() + def get_package_details(self): + if not self.healthcare_package: + return + + package_doc = frappe.get_doc("Healthcare Package", self.healthcare_package) + + self.package_details = [] + for item in package_doc.package_items: + self.append("package_details", (frappe.copy_doc(item)).as_dict()) + self.total_package_amount = package_doc.total_package_amount + self.outstanding_amount = package_doc.total_package_amount + self.discount_amount = package_doc.discount_amount + self.total_amount = package_doc.total_amount + + +@frappe.whitelist() +def create_payment_entry(package_subscription): + subscription_doc = frappe.get_doc("Package Subscription", package_subscription) + customer = frappe.db.get_value("Patient", subscription_doc.patient, "customer") + if customer: + payment_entry_doc = frappe.new_doc("Payment Entry") + payment_entry_doc.update( + { + "payment_type": "Receive", + "party_type": "Customer", + "party": customer, + "package_subscription": package_subscription, + "paid_amount": subscription_doc.outstanding_amount, + "received_amount": subscription_doc.outstanding_amount, + "target_exchange_rate": 1, + } + ) + + payment_entry_doc.insert(ignore_mandatory=True) + + return payment_entry_doc.name diff --git a/healthcare/healthcare/doctype/package_subscription/package_subscription_list.js b/healthcare/healthcare/doctype/package_subscription/package_subscription_list.js new file mode 100644 index 0000000000..96ef44097c --- /dev/null +++ b/healthcare/healthcare/doctype/package_subscription/package_subscription_list.js @@ -0,0 +1,15 @@ +frappe.listview_settings["Package Subscription"] = { + get_indicator: function (doc) { + if (doc.valid_to < frappe.datetime.get_today()) { + return [__("Expired"), "red", "valid_to,<," + frappe.datetime.get_today()]; + } else if (doc.status === "Paid") { + return [__("Paid"), "green", "status,=," + doc.status]; + } else if (doc.status === "Partially Paid") { + return [__("Partially Paid"), "orange", "status,=," + doc.status]; + } else if (doc.status === "Unpaid") { + return [__("Unpaid"), "red", "status,=," + doc.status]; + } else if (doc.status === "Discontinued") { + return [__("Discontinued"), "red", "status,=," + doc.status]; + } + }, +} \ No newline at end of file diff --git a/healthcare/healthcare/doctype/package_subscription/test_package_subscription.py b/healthcare/healthcare/doctype/package_subscription/test_package_subscription.py new file mode 100644 index 0000000000..dbe679daa7 --- /dev/null +++ b/healthcare/healthcare/doctype/package_subscription/test_package_subscription.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, earthians Health Informatics Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPackageSubscription(FrappeTestCase): + pass diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index e7af302a99..8b58d32e6d 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -40,6 +40,7 @@ def get_healthcare_services_to_invoice(patient, customer, company, link_customer items_to_invoice += get_therapy_sessions_to_invoice(patient, company) items_to_invoice += get_service_requests_to_invoice(patient, company) items_to_invoice += get_observations_to_invoice(patient, company) + items_to_invoice += get_package_subscriptions_to_invoice(patient, company) validate_customer_created(patient, customer, link_customer) return items_to_invoice @@ -436,6 +437,37 @@ def get_service_requests_to_invoice(patient, company): return orders_to_invoice +def get_package_subscriptions_to_invoice(patient, company): + subscriptions_to_invoice = [] + subscriptions = frappe.db.get_all( + "Package Subscription", + fields=["name", "healthcare_package"], + filters={ + "patient": patient.name, + "company": company, + "invoiced": False, + "docstatus": 1, + }, + ) + for sub in subscriptions: + subscription_doc = frappe.get_doc("Package Subscription", sub.name) + item, item_wise_invoicing = frappe.get_cached_value( + "Healthcare Package", sub.healthcare_package, ["item", "item_wise_invoicing"] + ) + if not item_wise_invoicing: + subscriptions_to_invoice.append( + {"reference_type": "Package Subscription", "reference_name": sub.name, "service": item} + ) + else: + for item in subscription_doc.package_details: + if not item.invoiced: + subscriptions_to_invoice.append( + {"reference_type": item.doctype, "reference_name": item.name, "service": item.item_code} + ) + + return subscriptions_to_invoice + + @frappe.whitelist() def get_appointment_billing_item_and_rate(doc): if isinstance(doc, str): @@ -663,6 +695,15 @@ def set_invoiced(item, method, ref_invoice=None): "Lab Test Template": "Lab Test" # 'Healthcare Service Unit': 'Inpatient Occupancy' } + elif item.reference_dt == "Healthcare Package Item": + frappe.db.set_value(item.reference_dt, item.reference_dn, "invoiced", invoiced) + + package_subsription = frappe.get_cached_value(item.reference_dt, item.reference_dn, "parent") + subsription_doc = frappe.get_doc("Package Subscription", package_subsription) + if any(item.get("invoiced") == 0 for item in subsription_doc.package_details): + frappe.db.set_value("Package Subscription", package_subsription, "invoiced", 0) + else: + frappe.db.set_value("Package Subscription", package_subsription, "invoiced", 1) def validate_invoiced_on_submit(item): diff --git a/healthcare/hooks.py b/healthcare/hooks.py index 7d8909318b..89db00624f 100644 --- a/healthcare/hooks.py +++ b/healthcare/hooks.py @@ -136,8 +136,8 @@ "after_insert": "healthcare.regional.india.abdm.utils.set_consent_attachment_details" }, "Payment Entry": { - "on_submit": "healthcare.healthcare.custom_doctype.payment_entry.set_paid_amount_in_treatment_counselling", - "on_cancel": "healthcare.healthcare.custom_doctype.payment_entry.set_paid_amount_in_treatment_counselling", + "on_submit": "healthcare.healthcare.custom_doctype.payment_entry.set_paid_amount_in_healthcare_docs", + "on_cancel": "healthcare.healthcare.custom_doctype.payment_entry.set_paid_amount_in_healthcare_docs", }, } diff --git a/healthcare/patches.txt b/healthcare/patches.txt index bda432c601..7c9f3b36ae 100644 --- a/healthcare/patches.txt +++ b/healthcare/patches.txt @@ -8,6 +8,7 @@ healthcare.patches.v15_0.rename_automate_appointment_invoicing healthcare.patches.v15_0.rename_medical_code_standard_and_medical_code healthcare.patches.v15_0.setup_service_request healthcare.patches.v15_0.create_custom_field_in_payment_entry +healthcare.patches.v15_0.create_custom_field_for_package_subscription [post_model_sync] healthcare.patches.v15_0.rename_field_medical_department_in_appoitment_type_service_item diff --git a/healthcare/patches/v15_0/create_custom_field_for_package_subscription.py b/healthcare/patches/v15_0/create_custom_field_for_package_subscription.py new file mode 100644 index 0000000000..82a0f9f536 --- /dev/null +++ b/healthcare/patches/v15_0/create_custom_field_for_package_subscription.py @@ -0,0 +1,18 @@ +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +def execute(): + custom_field = { + "Payment Entry": [ + { + "fieldname": "package_subscription", + "label": "Package Subscription", + "fieldtype": "Link", + "options": "Package Subscription", + "insert_after": "treatment_counselling", + "read_only": True, + }, + ] + } + + create_custom_fields(custom_field) diff --git a/healthcare/setup.py b/healthcare/setup.py index 156d0ad029..a690c63b49 100644 --- a/healthcare/setup.py +++ b/healthcare/setup.py @@ -136,6 +136,14 @@ "insert_after": "payment_order", "read_only": True, }, + { + "fieldname": "package_subscription", + "label": "Package Subscription", + "fieldtype": "Link", + "options": "Package Subscription", + "insert_after": "treatment_counselling", + "read_only": True, + }, ], }, "on_setup": "healthcare.setup.setup_healthcare", From b8a844b63998e68f87eb3cab0010947033eeddcf Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Sun, 1 Dec 2024 11:18:00 +0530 Subject: [PATCH 10/18] test: Healthcare Package --- .../test_healthcare_package.py | 115 +++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/healthcare/healthcare/doctype/healthcare_package/test_healthcare_package.py b/healthcare/healthcare/doctype/healthcare_package/test_healthcare_package.py index 7c97ac25bb..0e61226096 100644 --- a/healthcare/healthcare/doctype/healthcare_package/test_healthcare_package.py +++ b/healthcare/healthcare/doctype/healthcare_package/test_healthcare_package.py @@ -1,9 +1,120 @@ # Copyright (c) 2024, earthians Health Informatics Pvt. Ltd. and Contributors # See license.txt -# import frappe +import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_to_date, getdate + +from healthcare.healthcare.doctype.observation_template.test_observation_template import ( + create_observation_template, +) +from healthcare.healthcare.doctype.patient_appointment.test_patient_appointment import ( + create_patient, +) +from healthcare.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type class TestHealthcarePackage(FrappeTestCase): - pass + def test_healthcare_package(self): + frappe.delete_doc_if_exists("Healthcare Package", "Package - 1") + obs_name = "Total Cholesterol" + obs_template = create_observation_template(obs_name, "_Test") + patient = create_patient() + therapy_type = create_therapy_type() + income_account = frappe.db.exists( + "Account", {"root_type": "Income", "account_type": "Income Account"} + ) + + package = create_healthcare_package(therapy_type, obs_template, income_account) + + self.assertTrue(package.name) + self.assertTrue(frappe.db.exists("Item", package.item_code)) + + self.assertEqual( + package.total_package_amount, + 5200, + ) + + self.assertEqual( + frappe.db.get_value("Item Price", {"item_code": package.item_code}, "price_list_rate"), + package.total_package_amount, + ) + + subscription = create_subscription(patient, package, income_account) + + self.assertTrue(subscription.name) + self.assertEqual( + subscription.outstanding_amount, + subscription.total_package_amount, + ) + + self.assertEqual( + subscription.paid_amount, + 0, + ) + + +def create_healthcare_package(therapy_type=None, obs_template=None, income_account=None): + if not therapy_type or not obs_template: + return + + package = frappe.get_doc( + { + "doctype": "Healthcare Package", + "package_name": "Package - 1", + "price_list": "Standard Selling", + "currency": "INR", + "income_account": income_account, + "discount_amount": 100, + "item_code": "Package - 1", + "item_group": "Services", + "item_wise_invoicing": 0, + } + ) + package.append( + "package_items", + { + "package_item_type": "Observation Template", + "package_item": obs_template.name, + "no_of_sessions": 1, + "rate": obs_template.rate, + "amount": obs_template.rate, + "amount_with_discount": obs_template.rate, + }, + ) + package.append( + "package_items", + { + "package_item_type": "Therapy Type", + "package_item": therapy_type.name, + "no_of_sessions": 1, + "rate": therapy_type.rate, + "amount": therapy_type.rate, + "amount_with_discount": therapy_type.rate, + }, + ) + package.insert(ignore_permissions=True, ignore_mandatory=True) + + return package + + +def create_subscription(patient=None, package=None, income_account=None): + if not patient and not package.name: + return + + subscription = frappe.get_doc( + { + "doctype": "Package Subscription", + "healthcare_package": package.name, + "patient": patient, + "valid_to": add_to_date(getdate(), months=1), + "income_account": income_account, + "total_package_amount": package.total_package_amount, + } + ) + for item in package.package_items: + subscription.append("package_details", (frappe.copy_doc(item)).as_dict()) + + subscription.insert(ignore_permissions=True, ignore_mandatory=True) + + return subscription From 7098b7ac38cd68252fe61ab1a014fca8274dbe13 Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Sun, 1 Dec 2024 12:00:18 +0530 Subject: [PATCH 11/18] fix: add Healthcare Package shortcut to workspace --- .../workspace/healthcare/healthcare.json | 151 +++++++++--------- 1 file changed, 78 insertions(+), 73 deletions(-) diff --git a/healthcare/healthcare/workspace/healthcare/healthcare.json b/healthcare/healthcare/workspace/healthcare/healthcare.json index d24b7cfa93..116907040e 100644 --- a/healthcare/healthcare/workspace/healthcare/healthcare.json +++ b/healthcare/healthcare/workspace/healthcare/healthcare.json @@ -1,11 +1,12 @@ { + "app": "healthcare", "charts": [ { "chart_name": "Patient Appointments", "label": "Patient Appointments" } ], - "content": "[{\"id\":\"9b831922c0\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Healthcare\",\"col\":12}},{\"id\":\"f1f8e3812a\", \"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Patients\",\"col\":3}},{\"id\":\"326087361e\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Patients Admitted\",\"col\":3}},{\"id\":\"264b39da0e\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Open Appointments\",\"col\":3}},{\"id\":\"e507c90d85\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Appointments to Bill\",\"col\":3}},{\"id\":\"IPK_0OJyFF\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Patient Appointments\",\"col\":12}},{\"id\":\"fKaNGh_f3X\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"LCsKfCbHFS\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"gtytowgpr9\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Patient Appointment\",\"col\":4}},{\"id\":\"cgh4dqonE3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Patient\",\"col\":4}},{\"id\":\"pw8ECGStt-\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Healthcare Service Unit\",\"col\":4}},{\"id\":\"irCA7TCgAO\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Healthcare Practitioner\",\"col\":4}},{\"id\":\"wdgJqmB4mx\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Patient History\",\"col\":4}},{\"id\":\"oN5Gi4f4LN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"id\":\"QAyRuJGfxG\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"-K7NMpJiTM\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"M1PT6PedBj\",\"type\":\"card\",\"data\":{\"card_name\":\"Masters\",\"col\":4}},{\"id\":\"51eVEiCjvf\",\"type\":\"card\",\"data\":{\"card_name\":\"Consultation\",\"col\":4}},{\"id\":\"Mk_jHVQUPc\",\"type\":\"card\",\"data\":{\"card_name\":\"Orders\",\"col\":4}},{\"id\":\"PwTxarr1_m\",\"type\":\"card\",\"data\":{\"card_name\":\"Inpatient\",\"col\":4}},{\"id\":\"W-3YAoPKn-\",\"type\":\"card\",\"data\":{\"card_name\":\"Rehabilitation and Physiotherapy\",\"col\":4}},{\"id\":\"6cc3885df2\",\"type\":\"card\",\"data\":{\"card_name\":\"Diagnostic Module\",\"col\":4}},{\"id\":\"hMMLOCZpxJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Nursing\",\"col\":4}},{\"id\":\"jC32DydzyL\",\"type\":\"card\",\"data\":{\"card_name\":\"Laboratory\",\"col\":4}},{\"id\":\"AYDEjJSeTB\",\"type\":\"card\",\"data\":{\"card_name\":\"Medical Codes and Standards\",\"col\":4}},{\"id\":\"GvdoebZzcE\",\"type\":\"card\",\"data\":{\"card_name\":\"Service Units \",\"col\":4}},{\"id\":\"1f1f1a7b9e\",\"type\":\"card\",\"data\":{\"card_name\":\"Terminology Mapping\",\"col\":4}},{\"id\":\"rstxE68cTQ\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"YnekHjQCia\",\"type\":\"card\",\"data\":{\"card_name\":\"Consultation Setup\",\"col\":4}},{\"id\":\"5J3nicaW1a\",\"type\":\"card\",\"data\":{\"card_name\":\"Laboratory Setup\",\"col\":4}},{\"id\":\"zcOmhgxb5f\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", + "content": "[{\"id\":\"9b831922c0\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Healthcare\",\"col\":12}},{\"id\":\"f1f8e3812a\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Patients\",\"col\":3}},{\"id\":\"326087361e\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Patients Admitted\",\"col\":3}},{\"id\":\"264b39da0e\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Open Appointments\",\"col\":3}},{\"id\":\"e507c90d85\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Appointments to Bill\",\"col\":3}},{\"id\":\"IPK_0OJyFF\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Patient Appointments\",\"col\":12}},{\"id\":\"fKaNGh_f3X\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"LCsKfCbHFS\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"gtytowgpr9\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Patient Appointment\",\"col\":4}},{\"id\":\"cgh4dqonE3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Patient\",\"col\":4}},{\"id\":\"pw8ECGStt-\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Healthcare Service Unit\",\"col\":4}},{\"id\":\"irCA7TCgAO\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Healthcare Practitioner\",\"col\":4}},{\"id\":\"wdgJqmB4mx\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Patient History\",\"col\":4}},{\"id\":\"oN5Gi4f4LN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"id\":\"QAyRuJGfxG\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"-K7NMpJiTM\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"M1PT6PedBj\",\"type\":\"card\",\"data\":{\"card_name\":\"Masters\",\"col\":4}},{\"id\":\"51eVEiCjvf\",\"type\":\"card\",\"data\":{\"card_name\":\"Consultation\",\"col\":4}},{\"id\":\"Mk_jHVQUPc\",\"type\":\"card\",\"data\":{\"card_name\":\"Orders\",\"col\":4}},{\"id\":\"PwTxarr1_m\",\"type\":\"card\",\"data\":{\"card_name\":\"Inpatient\",\"col\":4}},{\"id\":\"W-3YAoPKn-\",\"type\":\"card\",\"data\":{\"card_name\":\"Rehabilitation and Physiotherapy\",\"col\":4}},{\"id\":\"6cc3885df2\",\"type\":\"card\",\"data\":{\"card_name\":\"Diagnostic Module\",\"col\":4}},{\"id\":\"hMMLOCZpxJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Nursing\",\"col\":4}},{\"id\":\"jC32DydzyL\",\"type\":\"card\",\"data\":{\"card_name\":\"Laboratory\",\"col\":4}},{\"id\":\"GvdoebZzcE\",\"type\":\"card\",\"data\":{\"card_name\":\"Service Units \",\"col\":4}},{\"id\":\"1f1f1a7b9e\",\"type\":\"card\",\"data\":{\"card_name\":\"Terminology Mapping\",\"col\":4}},{\"id\":\"rstxE68cTQ\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"YnekHjQCia\",\"type\":\"card\",\"data\":{\"card_name\":\"Consultation Setup\",\"col\":4}},{\"id\":\"5J3nicaW1a\",\"type\":\"card\",\"data\":{\"card_name\":\"Laboratory Setup\",\"col\":4}},{\"id\":\"zcOmhgxb5f\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", "creation": "2020-03-02 17:23:17.919682", "custom_blocks": [], "docstatus": 0, @@ -17,68 +18,6 @@ "is_hidden": 0, "label": "Healthcare", "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Masters", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Patient", - "link_count": 0, - "link_to": "Patient", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Healthcare Practitioner", - "link_count": 0, - "link_to": "Healthcare Practitioner", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Practitioner Schedule", - "link_count": 0, - "link_to": "Practitioner Schedule", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Medical Department", - "link_count": 0, - "link_to": "Medical Department", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Patient Care Type", - "link_count": 0, - "link_to": "Patient Care Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -326,14 +265,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Records and History", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -730,9 +661,82 @@ "link_type": "DocType", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Masters", + "link_count": 6, + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Patient", + "link_count": 0, + "link_to": "Patient", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Healthcare Practitioner", + "link_count": 0, + "link_to": "Healthcare Practitioner", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Practitioner Schedule", + "link_count": 0, + "link_to": "Practitioner Schedule", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Medical Department", + "link_count": 0, + "link_to": "Medical Department", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Patient Care Type", + "link_count": 0, + "link_to": "Patient Care Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Healthcare Package", + "link_count": 0, + "link_to": "Healthcare Package", + "link_type": "DocType", + "onboard": 0, + "type": "Link" } ], - "modified": "2024-05-14 00:39:38.432511", + "modified": "2024-12-01 11:58:00.855408", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare", @@ -809,5 +813,6 @@ "type": "Dashboard" } ], - "title": "Healthcare" + "title": "Healthcare", + "type": "Workspace" } \ No newline at end of file From 23ed975ffcd6758ef1148cd3e7c0728035d7d68c Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Thu, 5 Dec 2024 15:35:25 +0530 Subject: [PATCH 12/18] test: Package Subscription --- .../test_healthcare_package.py | 71 +----- .../test_package_subscription.py | 219 +++++++++++++++++- 2 files changed, 221 insertions(+), 69 deletions(-) diff --git a/healthcare/healthcare/doctype/healthcare_package/test_healthcare_package.py b/healthcare/healthcare/doctype/healthcare_package/test_healthcare_package.py index 0e61226096..c2e4988641 100644 --- a/healthcare/healthcare/doctype/healthcare_package/test_healthcare_package.py +++ b/healthcare/healthcare/doctype/healthcare_package/test_healthcare_package.py @@ -3,11 +3,14 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_to_date, getdate from healthcare.healthcare.doctype.observation_template.test_observation_template import ( create_observation_template, ) +from healthcare.healthcare.doctype.package_subscription.test_package_subscription import ( + create_healthcare_package, + create_subscription, +) from healthcare.healthcare.doctype.patient_appointment.test_patient_appointment import ( create_patient, ) @@ -52,69 +55,3 @@ def test_healthcare_package(self): subscription.paid_amount, 0, ) - - -def create_healthcare_package(therapy_type=None, obs_template=None, income_account=None): - if not therapy_type or not obs_template: - return - - package = frappe.get_doc( - { - "doctype": "Healthcare Package", - "package_name": "Package - 1", - "price_list": "Standard Selling", - "currency": "INR", - "income_account": income_account, - "discount_amount": 100, - "item_code": "Package - 1", - "item_group": "Services", - "item_wise_invoicing": 0, - } - ) - package.append( - "package_items", - { - "package_item_type": "Observation Template", - "package_item": obs_template.name, - "no_of_sessions": 1, - "rate": obs_template.rate, - "amount": obs_template.rate, - "amount_with_discount": obs_template.rate, - }, - ) - package.append( - "package_items", - { - "package_item_type": "Therapy Type", - "package_item": therapy_type.name, - "no_of_sessions": 1, - "rate": therapy_type.rate, - "amount": therapy_type.rate, - "amount_with_discount": therapy_type.rate, - }, - ) - package.insert(ignore_permissions=True, ignore_mandatory=True) - - return package - - -def create_subscription(patient=None, package=None, income_account=None): - if not patient and not package.name: - return - - subscription = frappe.get_doc( - { - "doctype": "Package Subscription", - "healthcare_package": package.name, - "patient": patient, - "valid_to": add_to_date(getdate(), months=1), - "income_account": income_account, - "total_package_amount": package.total_package_amount, - } - ) - for item in package.package_items: - subscription.append("package_details", (frappe.copy_doc(item)).as_dict()) - - subscription.insert(ignore_permissions=True, ignore_mandatory=True) - - return subscription diff --git a/healthcare/healthcare/doctype/package_subscription/test_package_subscription.py b/healthcare/healthcare/doctype/package_subscription/test_package_subscription.py index dbe679daa7..746c1fe8c9 100644 --- a/healthcare/healthcare/doctype/package_subscription/test_package_subscription.py +++ b/healthcare/healthcare/doctype/package_subscription/test_package_subscription.py @@ -1,9 +1,224 @@ # Copyright (c) 2024, earthians Health Informatics Pvt. Ltd. and Contributors # See license.txt -# import frappe +import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_to_date, getdate + +import erpnext + +from healthcare.healthcare.doctype.healthcare_settings.healthcare_settings import ( + get_income_account, + get_receivable_account, +) +from healthcare.healthcare.doctype.observation_template.test_observation_template import ( + create_observation_template, +) +from healthcare.healthcare.doctype.package_subscription.package_subscription import ( + create_payment_entry, +) +from healthcare.healthcare.doctype.patient_appointment.test_patient_appointment import ( + create_patient, +) +from healthcare.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type class TestPackageSubscription(FrappeTestCase): - pass + def test_package_subscription(self): + frappe.delete_doc_if_exists("Healthcare Package", "Package - 1") + obs_name = "Total Cholesterol" + obs_template = create_observation_template(obs_name, "_Test") + patient = create_patient() + therapy_type = create_therapy_type() + income_account = frappe.db.exists( + "Account", {"root_type": "Income", "account_type": "Income Account"} + ) + + package = create_healthcare_package(therapy_type, obs_template, income_account) + + self.assertTrue(package.name) + self.assertTrue(frappe.db.exists("Item", package.item_code)) + + subscription = create_subscription(patient, package, income_account) + + self.assertTrue(subscription.name) + self.assertEqual( + package.total_package_amount, + subscription.total_package_amount, + ) + + self.assertEqual( + subscription.paid_amount, + 0, + ) + + if subscription.docstatus == 0: + subscription.submit() + + payment_entry = create_payment_entry(subscription.name) + self.assertTrue(payment_entry) + + pe_doc = frappe.get_doc("Payment Entry", payment_entry) + mop_account = frappe.get_cached_value( + "Mode of Payment Account", {"company": pe_doc.company, "parent": "Cash"}, "default_account" + ) + if not mop_account: + mop_account = frappe.get_cached_value("Company", pe_doc.company, "default_cash_account") + paid_to_currency = frappe.get_cached_value("Account", mop_account, "account_currency") + if not paid_to_currency: + paid_to_currency = frappe.get_cached_value("Company", pe_doc.company, "default_currency") + + pe_doc.mode_of_payment = "Cash" + pe_doc.paid_to = mop_account + pe_doc.paid_to_account_currency = paid_to_currency + pe_doc.submit() + self.assertEqual(pe_doc.paid_amount, subscription.total_package_amount) + + self.assertEqual( + pe_doc.paid_amount, + frappe.get_cached_value("Package Subscription", subscription.name, "paid_amount"), + ) + + self.assertEqual( + frappe.get_cached_value("Package Subscription", subscription.name, "outstanding_amount"), 0 + ) + + sales_invoice = create_sales_invoice(patient, subscription.name) + + self.assertTrue(sales_invoice.name) + self.assertEqual(sales_invoice.grand_total, 5200) + + if sales_invoice: + self.assertEqual(sales_invoice.items[0].reference_dt, "Package Subscription") + + +def create_subscription(patient=None, package=None, income_account=None): + if not patient and not package.name: + return + + subscription = frappe.get_doc( + { + "doctype": "Package Subscription", + "healthcare_package": package.name, + "patient": patient, + "valid_to": add_to_date(getdate(), months=1), + "income_account": income_account, + "total_package_amount": package.total_package_amount, + } + ) + for item in package.package_items: + subscription.append("package_details", (frappe.copy_doc(item)).as_dict()) + + subscription.insert(ignore_permissions=True, ignore_mandatory=True) + + return subscription + + +def create_sales_invoice(patient, subscription=None): + if not subscription: + return + + subscriptions_to_invoice = [] + subscription_doc = frappe.get_cached_doc("Package Subscription", subscription) + item, item_wise_invoicing = frappe.get_cached_value( + "Healthcare Package", subscription_doc.healthcare_package, ["item", "item_wise_invoicing"] + ) + if not item_wise_invoicing: + subscriptions_to_invoice.append( + { + "reference_type": "Package Subscription", + "reference_name": subscription, + "service": item, + "qty": 1, + "amount": subscription_doc.total_package_amount, + } + ) + else: + for item in subscription_doc.package_details: + if not item.invoiced: + subscriptions_to_invoice.append( + { + "reference_type": item.doctype, + "reference_name": item.name, + "service": item.item_code, + "qty": item.no_of_sessions, + "amount": item.amount_with_discount, + } + ) + + sales_invoice = frappe.new_doc("Sales Invoice") + sales_invoice.patient = patient + sales_invoice.customer = frappe.get_cached_value("Patient", patient, "customer") + sales_invoice.due_date = getdate() + sales_invoice.currency = frappe.get_cached_value( + "Company", subscription_doc.company, "default_currency" + ) + sales_invoice.company = subscription_doc.company + sales_invoice.debit_to = get_receivable_account(subscription_doc.company) + for item in subscriptions_to_invoice: + sales_invoice.append( + "items", + { + "qty": item.get("qty"), + "uom": "Nos", + "conversion_factor": 1, + "income_account": get_income_account(None, subscription_doc.company), + "rate": item.get("amount") / item.get("qty"), + "amount": item.get("amount"), + "reference_dt": item.get("reference_type"), + "reference_dn": item.get("reference_name"), + "cost_center": erpnext.get_default_cost_center(subscription_doc.company), + "item_code": item.get("service"), + "item_name": item.get("service"), + "description": item.get("service"), + }, + ) + + sales_invoice.set_missing_values() + sales_invoice.submit() + + return sales_invoice + + +def create_healthcare_package(therapy_type=None, obs_template=None, income_account=None): + if not therapy_type or not obs_template: + return + + package = frappe.get_doc( + { + "doctype": "Healthcare Package", + "package_name": "Package - 1", + "price_list": "Standard Selling", + "currency": "INR", + "income_account": income_account, + "discount_amount": 100, + "item_code": "Package - 1", + "item_group": "Services", + "item_wise_invoicing": 0, + } + ) + package.append( + "package_items", + { + "package_item_type": "Observation Template", + "package_item": obs_template.name, + "no_of_sessions": 1, + "rate": obs_template.rate, + "amount": obs_template.rate, + "amount_with_discount": obs_template.rate, + }, + ) + package.append( + "package_items", + { + "package_item_type": "Therapy Type", + "package_item": therapy_type.name, + "no_of_sessions": 1, + "rate": therapy_type.rate, + "amount": therapy_type.rate, + "amount_with_discount": therapy_type.rate, + }, + ) + package.insert(ignore_permissions=True, ignore_mandatory=True) + + return package From 0cac796ffb377a274719916998954ecf020e46f0 Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Thu, 5 Dec 2024 15:37:45 +0530 Subject: [PATCH 13/18] refactor: replace direct assignment with frm.set_value in Healthcare Package --- .../doctype/healthcare_package/healthcare_package.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/healthcare/healthcare/doctype/healthcare_package/healthcare_package.js b/healthcare/healthcare/doctype/healthcare_package/healthcare_package.js index 05eb2c5de5..cec21b00d6 100644 --- a/healthcare/healthcare/doctype/healthcare_package/healthcare_package.js +++ b/healthcare/healthcare/doctype/healthcare_package/healthcare_package.js @@ -1,7 +1,7 @@ // Copyright (c) 2024, earthians Health Informatics Pvt. Ltd. and contributors // For license information, please see license.txt -let package_docs_filter = ["Item","Clinical Procedure Template", "Observation Template", "Therapy Type"] +let package_docs_filter = ["Item", "Clinical Procedure Template", "Observation Template", "Therapy Type"] frappe.ui.form.on("Healthcare Package", { refresh: function (frm) { @@ -53,7 +53,7 @@ frappe.ui.form.on("Healthcare Package", { frm.via_discount_percentage = true; if(frm.doc.discount_percentage && frm.doc.discount_amount) { - frm.doc.discount_amount = 0; + frm.set_value("discount_amount", 0); } let discount_field = frm.doc.apply_discount_on == "Total" ? "total_amount" : "net_total"; @@ -69,7 +69,7 @@ frappe.ui.form.on("Healthcare Package", { discount_amount: function (frm) { if (!frm.via_discount_percentage) { - frm.doc.additional_discount_percentage = 0; + frm.set_value("discount_percentage", 0); let discount_field = frm.doc.apply_discount_on == "Total" ? "total_amount" : "net_total"; var total = flt(frm.doc[discount_field]); var discount_percentage = (flt(frm.doc.discount_amount) / total) * 100; @@ -77,6 +77,12 @@ frappe.ui.form.on("Healthcare Package", { frm.set_value("discount_percentage", discount_percentage) calculate_total_payable(frm); } + }, + + package_name: function (frm) { + if (frm.doc.package_name && !frm.doc.item_code) { + frm.set_value("item_code", frm.doc.package_name); + } } }); From 22727e93ae84ab7d1883a1c0fcfc617bb13711f5 Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Thu, 5 Dec 2024 15:45:28 +0530 Subject: [PATCH 14/18] fix(Package Subscription): restrict payment for draft subscriptions --- .../doctype/package_subscription/package_subscription.js | 2 +- .../doctype/package_subscription/package_subscription.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/healthcare/healthcare/doctype/package_subscription/package_subscription.js b/healthcare/healthcare/doctype/package_subscription/package_subscription.js index 5172033ba1..8b7d2c5eac 100644 --- a/healthcare/healthcare/doctype/package_subscription/package_subscription.js +++ b/healthcare/healthcare/doctype/package_subscription/package_subscription.js @@ -20,7 +20,7 @@ frappe.ui.form.on("Package Subscription", { }, refresh: function (frm) { - if (frm.doc.outstanding_amount > 0) { + if (frm.doc.outstanding_amount > 0 && frm.doc.docstatus == 1) { frm.add_custom_button(__("Payment Entry"), function() { frappe.call({ method: "healthcare.healthcare.doctype.package_subscription.package_subscription.create_payment_entry", diff --git a/healthcare/healthcare/doctype/package_subscription/package_subscription.py b/healthcare/healthcare/doctype/package_subscription/package_subscription.py index 57d030145b..54c113ae69 100644 --- a/healthcare/healthcare/doctype/package_subscription/package_subscription.py +++ b/healthcare/healthcare/doctype/package_subscription/package_subscription.py @@ -8,6 +8,7 @@ class PackageSubscription(Document): def validate(self): + self.get_package_details() if self.total_package_amount: self.outstanding_amount = self.total_package_amount - self.paid_amount From 4abf85bbab3288dc869b666da456413d1ca9fc9e Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Wed, 20 Nov 2024 08:15:48 +0530 Subject: [PATCH 15/18] fix(Patient History): include observation details to Patient History --- .../patient_history_settings.py | 135 +++++++++++++++--- healthcare/patches.txt | 1 + .../add_observation_to_patient_history.py | 23 +++ healthcare/setup.py | 9 ++ 4 files changed, 146 insertions(+), 22 deletions(-) create mode 100644 healthcare/patches/v15_0/add_observation_to_patient_history.py diff --git a/healthcare/healthcare/doctype/patient_history_settings/patient_history_settings.py b/healthcare/healthcare/doctype/patient_history_settings/patient_history_settings.py index 3e33a42bed..61b5fd9f6b 100644 --- a/healthcare/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/healthcare/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -9,7 +9,9 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import cint, cstr +from frappe.utils.formatters import format_value +from healthcare.healthcare.doctype.observation.observation import get_observation_details from healthcare.healthcare.page.patient_history.patient_history import get_patient_history_doctypes @@ -79,31 +81,47 @@ def create_medical_record(doc, method=None): if not medical_record_required: return - if frappe.db.exists("Patient Medical Record", {"reference_name": doc.name}): + reference = doc.name + if doc.doctype == "Observation": + if doc.reference_docname or doc.sales_invoice: + ref_docname = doc.reference_docname + if doc.sales_invoice: + ref_docname = doc.sales_invoice + + reference = frappe.db.exists("Diagnostic Report", {"docname": ref_docname}) + + if frappe.db.exists("Patient Medical Record", {"reference_name": reference}): + if doc.doctype == "Observation" and reference: + update_medical_record(doc, reference=reference) return - subject = set_subject_field(doc) + subject = set_subject_field(doc, reference) date_field = get_date_field(doc.doctype) medical_record = frappe.new_doc("Patient Medical Record") medical_record.patient = doc.patient medical_record.subject = subject medical_record.status = "Open" medical_record.communication_date = doc.get(date_field) - medical_record.reference_doctype = doc.doctype - medical_record.reference_name = doc.name + medical_record.reference_doctype = ( + doc.doctype if doc.doctype != "Observation" else "Diagnostic Report" + ) + medical_record.reference_name = doc.name if doc.doctype != "Observation" else reference medical_record.reference_owner = doc.owner medical_record.save(ignore_permissions=True) -def update_medical_record(doc, method=None): +def update_medical_record(doc, method=None, reference=None): medical_record_required = validate_medical_record_required(doc) if not medical_record_required: return - medical_record_id = frappe.db.exists("Patient Medical Record", {"reference_name": doc.name}) + medical_record_id = frappe.db.exists( + "Patient Medical Record", + {"reference_name": reference if doc.doctype == "Observation" else doc.name}, + ) if medical_record_id: - subject = set_subject_field(doc) + subject = set_subject_field(doc, reference) frappe.db.set_value("Patient Medical Record", medical_record_id, "subject", subject) else: create_medical_record(doc) @@ -119,25 +137,27 @@ def delete_medical_record(doc, method=None): frappe.delete_doc("Patient Medical Record", record, force=1) -def set_subject_field(doc): - from frappe.utils.formatters import format_value - +def set_subject_field(doc, reference=None): meta = frappe.get_meta(doc.doctype) subject = "" patient_history_fields = get_patient_history_fields(doc) - - for entry in patient_history_fields: - fieldname = entry.get("fieldname") - if entry.get("fieldtype") == "Table" and doc.get(fieldname): - formatted_value = get_formatted_value_for_table_field( - doc.get(fieldname), meta.get_field(fieldname) - ) - subject += frappe.bold(_(entry.get("label")) + ":") + "
" + cstr(formatted_value) + "
" - + if doc.doctype == "Observation": + if doc.reference_docname or doc.sales_invoice: + subject = get_observation_subject_and_ref(patient_history_fields, reference, meta) else: - if doc.get(fieldname): - formatted_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc) - subject += frappe.bold(_(entry.get("label")) + ":") + cstr(formatted_value) + "
" + return + else: + for entry in patient_history_fields: + fieldname = entry.get("fieldname") + if entry.get("fieldtype") == "Table" and doc.get(fieldname): + formatted_value = get_formatted_value_for_table_field( + doc.get(fieldname), meta.get_field(fieldname) + ) + subject += frappe.bold(_(entry.get("label")) + ":") + "
" + cstr(formatted_value) + "
" + else: + if doc.get(fieldname): + formatted_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc) + subject += frappe.bold(_(entry.get("label")) + ":") + cstr(formatted_value) + "
" return subject @@ -213,3 +233,74 @@ def get_module(doc): module = frappe.db.get_value("DocType", doc.doctype, "module") return module + + +def get_observation_subject_and_ref(patient_history_fields, reference, meta): + if not reference: + return + + obs_data = get_observation_details(reference) + + subject = "" + for entry in obs_data[0]: + parent_obs = entry.get("observation") + + if entry.get("has_component"): + formatted_value = get_formatted_value_for_observations( + entry.get(parent_obs), patient_history_fields, meta + ) + subject += f"Observation: {parent_obs}
{cstr(formatted_value)}
" + else: + formatted_value = get_formatted_value_for_observations([entry], patient_history_fields, meta) + subject += f"Observation: {parent_obs.get('name')}
{cstr(formatted_value)}
" + + return subject + + +def get_formatted_value_for_observations(items, patient_history_fields, meta): + result_fields = [ + "result_boolean", + "result_data", + "result_text", + "result_float", + "result_select", + ] + table_head = "" + table_row = "" + html = "" + create_head = True + for item in items: + obs_details = item.get("observation") + table_row += "" + for key in patient_history_fields: + label, fieldname = key.get("label"), key.get("fieldname") + if create_head: + table_head += f"{label} " + if obs_details.get(fieldname): + if key.get("fieldtype") == "Datetime": + formatted_value = format_value(obs_details.get(fieldname), meta.get_field(fieldname)) + table_row += f"{str(formatted_value)}" + else: + table_row += f"{str(obs_details.get(fieldname))}" + else: + table_row += "" + + result = None + for field in result_fields: + if obs_details.get(field): + result = obs_details.get(field) + break + if create_head: + table_head += "Result" + if result: + table_row += f"{str(result)}" + + create_head = False + table_row += "" + + html += f""" + + {table_head} {table_row} +
""" + + return html diff --git a/healthcare/patches.txt b/healthcare/patches.txt index bda432c601..6a21502838 100644 --- a/healthcare/patches.txt +++ b/healthcare/patches.txt @@ -8,6 +8,7 @@ healthcare.patches.v15_0.rename_automate_appointment_invoicing healthcare.patches.v15_0.rename_medical_code_standard_and_medical_code healthcare.patches.v15_0.setup_service_request healthcare.patches.v15_0.create_custom_field_in_payment_entry +healthcare.patches.v15_0.add_observation_to_patient_history [post_model_sync] healthcare.patches.v15_0.rename_field_medical_department_in_appoitment_type_service_item diff --git a/healthcare/patches/v15_0/add_observation_to_patient_history.py b/healthcare/patches/v15_0/add_observation_to_patient_history.py new file mode 100644 index 0000000000..a7802e0144 --- /dev/null +++ b/healthcare/patches/v15_0/add_observation_to_patient_history.py @@ -0,0 +1,23 @@ +import json + +import frappe + + +def execute(): + settings = frappe.get_single("Patient History Settings") + selected_fields = [ + {"label": "Observation Template", "fieldname": "observation_template", "fieldtype": "Link"}, + {"label": "Posting Date", "fieldname": "posting_date", "fieldtype": "Date"}, + {"label": "Status", "fieldname": "status", "fieldtype": "Select"}, + {"label": "Time of Result", "fieldname": "time_of_result", "fieldtype": "Datetime"}, + ] + + settings.append( + "standard_doctypes", + { + "document_type": "Observation", + "date_fieldname": "posting_date", + "selected_fields": json.dumps(selected_fields), + }, + ) + settings.save() diff --git a/healthcare/setup.py b/healthcare/setup.py index 156d0ad029..45cc76db3e 100644 --- a/healthcare/setup.py +++ b/healthcare/setup.py @@ -984,6 +984,15 @@ def get_patient_history_config(): {"label": "Total Orders", "fieldname": "total_orders", "fieldtype": "Float"}, ], ), + "Observation": ( + "posting_date", + [ + {"label": "Observation Template", "fieldname": "observation_template", "fieldtype": "Link"}, + {"label": "Posting Date", "fieldname": "posting_date", "fieldtype": "Date"}, + {"label": "Status", "fieldname": "status", "fieldtype": "Select"}, + {"label": "Time of Result", "fieldname": "time_of_result", "fieldtype": "Datetime"}, + ], + ), } From 4d530a5a5740291a46916ebd71161ada1156e36a Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Mon, 9 Dec 2024 15:41:59 +0530 Subject: [PATCH 16/18] fix(Patient History): update subject rendering and patch to add Patient Medical Record for observations --- .../diagnostic_report/diagnostic_report.html | 411 ++++++++++++++++++ .../patient_history_settings.py | 77 +--- healthcare/patches.txt | 1 + ...atient_medical_records_for_observations.py | 21 + 4 files changed, 437 insertions(+), 73 deletions(-) create mode 100644 healthcare/healthcare/doctype/diagnostic_report/diagnostic_report.html create mode 100644 healthcare/patches/v15_0/create_patient_medical_records_for_observations.py diff --git a/healthcare/healthcare/doctype/diagnostic_report/diagnostic_report.html b/healthcare/healthcare/doctype/diagnostic_report/diagnostic_report.html new file mode 100644 index 0000000000..63a9f5af4e --- /dev/null +++ b/healthcare/healthcare/doctype/diagnostic_report/diagnostic_report.html @@ -0,0 +1,411 @@ + + + +{% set dob = frappe.db.get_value("Patient", doc.patient, "dob") %} +{% set years = 0 %} +{% set months = 0 %} +{% set days = 0 %} +{% if dob %} + {% set now = frappe.utils.nowdate() %} + {% if doc.ref_doctype=="Sales Invoice" and doc.docname %} + {% set now = frappe.db.get_value("Sales Invoice", doc.docname, "posting_date") %} + {% endif %} + {% set diff = frappe.utils.date_diff(now, dob) %} + {% set years = diff//365 %} + {% set months = (diff - (years * 365))//30 %} + {% set days = ( (diff - (years * 365)) - (months * 30) ) %} +{% endif %} + +{% set sex = frappe.db.get_value("Patient", doc.patient, "sex") %} + +{% set salutation = "Ms" %} + +{%- if years <= 1 -%} + {% set salutation = "Baby" %} +{% elif sex == "Male" and years <= 13%} + {% set salutation = "Master" %} +{% elif sex == "Female" and years <= 18 %} + {% set salutation = "Miss" %} +{% elif sex == "Male" %} + {% set salutation = "Mr" %} +{% endif %} +

DEPARTMENT OF LABORATORY MEDICINE

+
+
+
+
Patient name:
+
{{ salutation }} {{ doc.patient_name.upper() }}
+
+
+
Patient:
+
{{ doc.patient }}
+
+
+
Age/Gender:
+
{{ years }}Y {{ months }}M {{ days }}D / {{ sex }}
+
+
+
+
+
Referred By:
+ {%- if doc.practitioner_name -%} +
{{ doc.practitioner_name.upper() }}
+ {% else %} +
Self
+ {% endif %} +
+
+
Invoice No.:
+
{{ doc.docname }}
+
+
+
Invoiced On:
+
{{ frappe.utils.format_date(now) }}
+
+ +
+
+
+
+
+ + SAMPLE + +
+
+ Collected on +
+
+
+
+ + INVESTIGATION + +
+
+ Method +
+
+
+
+ + RESULT + +
+
+ Reported on +
+
+
+
+ + UNIT + +
+
+
+
+ + REFERENCE INTERVAL + +
+
+
+ +{% if doc.get("docname") %} + {% if doc.get("docname") %} + {% set full_data = diagnostic_report_print(doc.name) %} +
+ {% for data in full_data[0] %} + {% if not data.get("has_component") %} + {% if data.get("observation").get("preferred_display_name") %} + {% set observation_name = data.get("observation").get("preferred_display_name") %} + {% else %} + {% set observation_name = data.get("observation").get("observation_template") %} + {% endif %} + + {% if data.get("observation") or data.get("observation") %} + {% if data.get("observation").get("status")=="Approved" and (data.get("observation").get("result_data") or data.get("observation").get("result_text") or data.get("observation").get("result_select") not in [None, "", "Null"]) %} +
+
+ + {{observation_name}} + +
+
+
+
+ {% if data.get("observation").get("sample") %} +
+ {{data.get("observation").get("sample")}} +
+ {% else %} +
+ {{ frappe.db.get_value("Observation Template", data.get("observation").get("observation_template"), "sample") or "" }} +
+ {% endif %} + {% if data.get("observation").get("received_time") %} +
+ {{frappe.utils.format_datetime(data.get("observation").get("received_time"))[:-3]}} +
+ {% endif %} +
+
+
+ {{observation_name}} +
+ {% if data.get("observation").get("method") %} +
+ {{data.get("observation").get("method")}} +
+ {% endif %} +
+
+
+ {% if data.get("observation").get("result_data") or data.get("observation").get("result_select") %} + {{data.get("observation").get("result_data") + or data.get("observation").get("result_select")}} + {% elif data.get("observation").get("result_text") %} + {% if '
' in data.get("observation").get("result_text") %} + {% if data.get("observation").get("result_text")|length <= 60 %} + {{data.get("observation").get("result_text")}} + {% endif %} + {% elif data.get("observation").get("result_text")|length <= 24 %} + {{data.get("observation").get("result_text")}} + {% endif %} + {% endif %} +
+ {% if data.get("observation").get("time_of_result") %} +
+ {{frappe.utils.format_datetime(data.get("observation").get("time_of_result"))[:-3]}} +
+ {% endif %} +
+
+ {% if data.get("observation").get("permitted_unit") %} + {{data.get("observation").get("permitted_unit")}} + {% endif %} +
+
+ {% if data.get("observation").get("reference") %} + {{data.get("observation").get("reference")}} + {% endif %} +
+
+ {% if data.get("observation").get("result_text") %} + {% if '
' in data.get("observation").get("result_text") %} + {% if data.get("observation").get("result_text")|length > 60 %} +
+ {{data.get("observation").get("result_text")}} +
+ {% endif %} + {% elif data.get("observation").get("result_text")|length > 24 %} +
+ {{data.get("observation").get("result_text")}} +
+ {% endif %} + {% endif %} + {% if data.get("observation").get("result_interpretation") %} +
+ {{data.get("observation").get("result_interpretation")}} +
+ {% endif %} + {% if data.get("observation").get("note") %} +
+ {{data.get("observation").get("note")}} +
+ {% endif %} + {% if data.get("observation").get("description") %} +
+ {{data.get("observation").get("description")}} +
+ {% endif %} +
+ + {% endif %} + {% endif %} + {% else %} + {% if data["obs_approved"] and data[data.get("observation")] and data["has_result"] %} +
+
+
+
+
+ + {{data.get("display_name")}} + +
+ {% for comps in data[data.get("observation")] %} + {% if comps.get("observation").get("preferred_display_name") %} + {% set observation_name = comps.get("observation").get("preferred_display_name") %} + {% else %} + {% set observation_name = comps.get("observation").get("observation_template") %} + {% endif %} + {% if comps.get("observation").get("status")=="Approved" and comps.get("observation") %} + {% if comps.get("observation").get("result_data") or comps.get("observation").get("result_text") or comps.get("observation").get("result_select") not in [None, "", "Null"] %} +
+
+
+
+ {% if comps.get("observation").get("sample") %} +
+ {{comps.get("observation").get("sample")}} +
+ {% else %} +
+ {{ frappe.db.get_value("Observation Template", comps.get("observation").get("observation_template"), "sample") or ""}} +
+ {% endif %} + {% if comps.get("observation").get("received_time") %} +
+ {{frappe.utils.format_datetime(comps.get("observation").get("received_time"))[:-3]}} +
+ {% endif %} +
+
+
+ {{observation_name}} +
+ {% if comps.get("observation").get("method") %} +
+ {{comps.get("observation").get("method")}} +
+ {% endif %} +
+
+
+ {% if comps.get("observation").get("result_data") or comps.get("observation").get("result_select") %} + {{comps.get("observation").get("result_data") + or comps.get("observation").get("result_select")}} + {% elif comps.get("observation").get("result_text") %} + {% if '
' in comps.get("observation").get("result_text") %} + {% if comps.get("observation").get("result_text")|length <= 60 %} + {{comps.get("observation").get("result_text")}} + {% endif %} + {% elif comps.get("observation").get("result_text")|length <= 24 %} + {{comps.get("observation").get("result_text")}} + {% endif %} + {% endif %} +
+ {% if comps.get("observation").get("time_of_result") %} +
+ {{frappe.utils.format_datetime(comps.get("observation").get("time_of_result"))[:-3]}} +
+ {% endif %} +
+
+ {% if comps.get("observation").get("permitted_unit") %} + {{comps.get("observation").get("permitted_unit")}} + {% endif %} +
+
+ {% if comps.get("observation").get("reference") %} + {{comps.get("observation").get("reference")}} + {% endif %} +
+
+ {% if comps.get("observation").get("result_text") %} + {% if '
' in comps.get("observation").get("result_text") %} + {% if comps.get("observation").get("result_text")|length > 60 %} +
+ {{comps.get("observation").get("result_text")}} +
+ {% endif %} + {% elif comps.get("observation").get("result_text")|length > 24 %} +
+ {{comps.get("observation").get("result_text")}} +
+ {% endif %} + {% endif %} + {% if comps.get("observation").get("result_interpretation") %} +
+ {{comps.get("observation").get("result_interpretation")}} +
+ {% endif %} + {% if comps.get("observation").get("note") %} +
+ {{comps.get("observation").get("note")}} +
+ {% endif %} + {% if comps.get("observation").get("description") and not data.get("description") %} +
+ {{comps.get("observation").get("description")}} +
+ {% endif %} +
+
+ {% endif %} + {% endif %} + {% endfor %} +
+ {{data.get("description") or ""}} +
+
+
+ + + {% endif %} + {% endif %} + {% endfor %} + {% if full_data[0]|length > 0 %} +
* End of Report *
+ {% endif %} + + {% endif %} +{% endif %} \ No newline at end of file diff --git a/healthcare/healthcare/doctype/patient_history_settings/patient_history_settings.py b/healthcare/healthcare/doctype/patient_history_settings/patient_history_settings.py index 61b5fd9f6b..79ee6b41aa 100644 --- a/healthcare/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/healthcare/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -11,7 +11,6 @@ from frappe.utils import cint, cstr from frappe.utils.formatters import format_value -from healthcare.healthcare.doctype.observation.observation import get_observation_details from healthcare.healthcare.page.patient_history.patient_history import get_patient_history_doctypes @@ -143,7 +142,10 @@ def set_subject_field(doc, reference=None): patient_history_fields = get_patient_history_fields(doc) if doc.doctype == "Observation": if doc.reference_docname or doc.sales_invoice: - subject = get_observation_subject_and_ref(patient_history_fields, reference, meta) + doc = frappe.get_doc("Diagnostic Report", reference) + subject = frappe.render_template( + "healthcare/healthcare/doctype/diagnostic_report/diagnostic_report.html", dict(doc=doc) + ) else: return else: @@ -233,74 +235,3 @@ def get_module(doc): module = frappe.db.get_value("DocType", doc.doctype, "module") return module - - -def get_observation_subject_and_ref(patient_history_fields, reference, meta): - if not reference: - return - - obs_data = get_observation_details(reference) - - subject = "" - for entry in obs_data[0]: - parent_obs = entry.get("observation") - - if entry.get("has_component"): - formatted_value = get_formatted_value_for_observations( - entry.get(parent_obs), patient_history_fields, meta - ) - subject += f"Observation: {parent_obs}
{cstr(formatted_value)}
" - else: - formatted_value = get_formatted_value_for_observations([entry], patient_history_fields, meta) - subject += f"Observation: {parent_obs.get('name')}
{cstr(formatted_value)}
" - - return subject - - -def get_formatted_value_for_observations(items, patient_history_fields, meta): - result_fields = [ - "result_boolean", - "result_data", - "result_text", - "result_float", - "result_select", - ] - table_head = "" - table_row = "" - html = "" - create_head = True - for item in items: - obs_details = item.get("observation") - table_row += "" - for key in patient_history_fields: - label, fieldname = key.get("label"), key.get("fieldname") - if create_head: - table_head += f"{label} " - if obs_details.get(fieldname): - if key.get("fieldtype") == "Datetime": - formatted_value = format_value(obs_details.get(fieldname), meta.get_field(fieldname)) - table_row += f"{str(formatted_value)}" - else: - table_row += f"{str(obs_details.get(fieldname))}" - else: - table_row += "" - - result = None - for field in result_fields: - if obs_details.get(field): - result = obs_details.get(field) - break - if create_head: - table_head += "Result" - if result: - table_row += f"{str(result)}" - - create_head = False - table_row += "" - - html += f""" - - {table_head} {table_row} -
""" - - return html diff --git a/healthcare/patches.txt b/healthcare/patches.txt index 6a21502838..a90e136498 100644 --- a/healthcare/patches.txt +++ b/healthcare/patches.txt @@ -9,6 +9,7 @@ healthcare.patches.v15_0.rename_medical_code_standard_and_medical_code healthcare.patches.v15_0.setup_service_request healthcare.patches.v15_0.create_custom_field_in_payment_entry healthcare.patches.v15_0.add_observation_to_patient_history +healthcare.patches.v15_0.create_patient_medical_records_for_observations [post_model_sync] healthcare.patches.v15_0.rename_field_medical_department_in_appoitment_type_service_item diff --git a/healthcare/patches/v15_0/create_patient_medical_records_for_observations.py b/healthcare/patches/v15_0/create_patient_medical_records_for_observations.py new file mode 100644 index 0000000000..e5691d40c0 --- /dev/null +++ b/healthcare/patches/v15_0/create_patient_medical_records_for_observations.py @@ -0,0 +1,21 @@ +import frappe + + +def execute(): + diagnostic_report = frappe.db.get_all("Diagnostic Report", pluck="name") + + for diag in diagnostic_report: + diag_doc = frappe.get_doc("Diagnostic Report", diag) + + subject = frappe.render_template( + "healthcare/healthcare/doctype/diagnostic_report/diagnostic_report.html", dict(doc=diag_doc) + ) + medical_record = frappe.new_doc("Patient Medical Record") + medical_record.patient = diag_doc.patient + medical_record.subject = subject + medical_record.status = "Open" + medical_record.communication_date = diag_doc.reference_posting_date + medical_record.reference_doctype = "Diagnostic Report" + medical_record.reference_name = diag_doc.name + medical_record.reference_owner = diag_doc.owner + medical_record.save(ignore_permissions=True) From ec5276281f0836d8252440f29e69008ba2c05244 Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Tue, 17 Dec 2024 15:12:35 +0530 Subject: [PATCH 17/18] fix(Observation): change reference of diagnostic report to observation and update patch --- .../diagnostic_report/diagnostic_report.html | 411 ------------------ .../doctype/observation/observation.html | 360 +++++++++++++++ .../doctype/observation/observation.py | 14 + .../patient_history_settings.py | 30 +- healthcare/hooks.py | 1 + ...atient_medical_records_for_observations.py | 39 +- 6 files changed, 411 insertions(+), 444 deletions(-) delete mode 100644 healthcare/healthcare/doctype/diagnostic_report/diagnostic_report.html create mode 100644 healthcare/healthcare/doctype/observation/observation.html diff --git a/healthcare/healthcare/doctype/diagnostic_report/diagnostic_report.html b/healthcare/healthcare/doctype/diagnostic_report/diagnostic_report.html deleted file mode 100644 index 63a9f5af4e..0000000000 --- a/healthcare/healthcare/doctype/diagnostic_report/diagnostic_report.html +++ /dev/null @@ -1,411 +0,0 @@ - - - -{% set dob = frappe.db.get_value("Patient", doc.patient, "dob") %} -{% set years = 0 %} -{% set months = 0 %} -{% set days = 0 %} -{% if dob %} - {% set now = frappe.utils.nowdate() %} - {% if doc.ref_doctype=="Sales Invoice" and doc.docname %} - {% set now = frappe.db.get_value("Sales Invoice", doc.docname, "posting_date") %} - {% endif %} - {% set diff = frappe.utils.date_diff(now, dob) %} - {% set years = diff//365 %} - {% set months = (diff - (years * 365))//30 %} - {% set days = ( (diff - (years * 365)) - (months * 30) ) %} -{% endif %} - -{% set sex = frappe.db.get_value("Patient", doc.patient, "sex") %} - -{% set salutation = "Ms" %} - -{%- if years <= 1 -%} - {% set salutation = "Baby" %} -{% elif sex == "Male" and years <= 13%} - {% set salutation = "Master" %} -{% elif sex == "Female" and years <= 18 %} - {% set salutation = "Miss" %} -{% elif sex == "Male" %} - {% set salutation = "Mr" %} -{% endif %} -

DEPARTMENT OF LABORATORY MEDICINE

-
-
-
-
Patient name:
-
{{ salutation }} {{ doc.patient_name.upper() }}
-
-
-
Patient:
-
{{ doc.patient }}
-
-
-
Age/Gender:
-
{{ years }}Y {{ months }}M {{ days }}D / {{ sex }}
-
-
-
-
-
Referred By:
- {%- if doc.practitioner_name -%} -
{{ doc.practitioner_name.upper() }}
- {% else %} -
Self
- {% endif %} -
-
-
Invoice No.:
-
{{ doc.docname }}
-
-
-
Invoiced On:
-
{{ frappe.utils.format_date(now) }}
-
- -
-
-
-
-
- - SAMPLE - -
-
- Collected on -
-
-
-
- - INVESTIGATION - -
-
- Method -
-
-
-
- - RESULT - -
-
- Reported on -
-
-
-
- - UNIT - -
-
-
-
- - REFERENCE INTERVAL - -
-
-
- -{% if doc.get("docname") %} - {% if doc.get("docname") %} - {% set full_data = diagnostic_report_print(doc.name) %} -
- {% for data in full_data[0] %} - {% if not data.get("has_component") %} - {% if data.get("observation").get("preferred_display_name") %} - {% set observation_name = data.get("observation").get("preferred_display_name") %} - {% else %} - {% set observation_name = data.get("observation").get("observation_template") %} - {% endif %} - - {% if data.get("observation") or data.get("observation") %} - {% if data.get("observation").get("status")=="Approved" and (data.get("observation").get("result_data") or data.get("observation").get("result_text") or data.get("observation").get("result_select") not in [None, "", "Null"]) %} -
-
- - {{observation_name}} - -
-
-
-
- {% if data.get("observation").get("sample") %} -
- {{data.get("observation").get("sample")}} -
- {% else %} -
- {{ frappe.db.get_value("Observation Template", data.get("observation").get("observation_template"), "sample") or "" }} -
- {% endif %} - {% if data.get("observation").get("received_time") %} -
- {{frappe.utils.format_datetime(data.get("observation").get("received_time"))[:-3]}} -
- {% endif %} -
-
-
- {{observation_name}} -
- {% if data.get("observation").get("method") %} -
- {{data.get("observation").get("method")}} -
- {% endif %} -
-
-
- {% if data.get("observation").get("result_data") or data.get("observation").get("result_select") %} - {{data.get("observation").get("result_data") - or data.get("observation").get("result_select")}} - {% elif data.get("observation").get("result_text") %} - {% if '
' in data.get("observation").get("result_text") %} - {% if data.get("observation").get("result_text")|length <= 60 %} - {{data.get("observation").get("result_text")}} - {% endif %} - {% elif data.get("observation").get("result_text")|length <= 24 %} - {{data.get("observation").get("result_text")}} - {% endif %} - {% endif %} -
- {% if data.get("observation").get("time_of_result") %} -
- {{frappe.utils.format_datetime(data.get("observation").get("time_of_result"))[:-3]}} -
- {% endif %} -
-
- {% if data.get("observation").get("permitted_unit") %} - {{data.get("observation").get("permitted_unit")}} - {% endif %} -
-
- {% if data.get("observation").get("reference") %} - {{data.get("observation").get("reference")}} - {% endif %} -
-
- {% if data.get("observation").get("result_text") %} - {% if '
' in data.get("observation").get("result_text") %} - {% if data.get("observation").get("result_text")|length > 60 %} -
- {{data.get("observation").get("result_text")}} -
- {% endif %} - {% elif data.get("observation").get("result_text")|length > 24 %} -
- {{data.get("observation").get("result_text")}} -
- {% endif %} - {% endif %} - {% if data.get("observation").get("result_interpretation") %} -
- {{data.get("observation").get("result_interpretation")}} -
- {% endif %} - {% if data.get("observation").get("note") %} -
- {{data.get("observation").get("note")}} -
- {% endif %} - {% if data.get("observation").get("description") %} -
- {{data.get("observation").get("description")}} -
- {% endif %} -
- - {% endif %} - {% endif %} - {% else %} - {% if data["obs_approved"] and data[data.get("observation")] and data["has_result"] %} -
-
-
-
-
- - {{data.get("display_name")}} - -
- {% for comps in data[data.get("observation")] %} - {% if comps.get("observation").get("preferred_display_name") %} - {% set observation_name = comps.get("observation").get("preferred_display_name") %} - {% else %} - {% set observation_name = comps.get("observation").get("observation_template") %} - {% endif %} - {% if comps.get("observation").get("status")=="Approved" and comps.get("observation") %} - {% if comps.get("observation").get("result_data") or comps.get("observation").get("result_text") or comps.get("observation").get("result_select") not in [None, "", "Null"] %} -
-
-
-
- {% if comps.get("observation").get("sample") %} -
- {{comps.get("observation").get("sample")}} -
- {% else %} -
- {{ frappe.db.get_value("Observation Template", comps.get("observation").get("observation_template"), "sample") or ""}} -
- {% endif %} - {% if comps.get("observation").get("received_time") %} -
- {{frappe.utils.format_datetime(comps.get("observation").get("received_time"))[:-3]}} -
- {% endif %} -
-
-
- {{observation_name}} -
- {% if comps.get("observation").get("method") %} -
- {{comps.get("observation").get("method")}} -
- {% endif %} -
-
-
- {% if comps.get("observation").get("result_data") or comps.get("observation").get("result_select") %} - {{comps.get("observation").get("result_data") - or comps.get("observation").get("result_select")}} - {% elif comps.get("observation").get("result_text") %} - {% if '
' in comps.get("observation").get("result_text") %} - {% if comps.get("observation").get("result_text")|length <= 60 %} - {{comps.get("observation").get("result_text")}} - {% endif %} - {% elif comps.get("observation").get("result_text")|length <= 24 %} - {{comps.get("observation").get("result_text")}} - {% endif %} - {% endif %} -
- {% if comps.get("observation").get("time_of_result") %} -
- {{frappe.utils.format_datetime(comps.get("observation").get("time_of_result"))[:-3]}} -
- {% endif %} -
-
- {% if comps.get("observation").get("permitted_unit") %} - {{comps.get("observation").get("permitted_unit")}} - {% endif %} -
-
- {% if comps.get("observation").get("reference") %} - {{comps.get("observation").get("reference")}} - {% endif %} -
-
- {% if comps.get("observation").get("result_text") %} - {% if '
' in comps.get("observation").get("result_text") %} - {% if comps.get("observation").get("result_text")|length > 60 %} -
- {{comps.get("observation").get("result_text")}} -
- {% endif %} - {% elif comps.get("observation").get("result_text")|length > 24 %} -
- {{comps.get("observation").get("result_text")}} -
- {% endif %} - {% endif %} - {% if comps.get("observation").get("result_interpretation") %} -
- {{comps.get("observation").get("result_interpretation")}} -
- {% endif %} - {% if comps.get("observation").get("note") %} -
- {{comps.get("observation").get("note")}} -
- {% endif %} - {% if comps.get("observation").get("description") and not data.get("description") %} -
- {{comps.get("observation").get("description")}} -
- {% endif %} -
-
- {% endif %} - {% endif %} - {% endfor %} -
- {{data.get("description") or ""}} -
-
-
- - - {% endif %} - {% endif %} - {% endfor %} - {% if full_data[0]|length > 0 %} -
* End of Report *
- {% endif %} - - {% endif %} -{% endif %} \ No newline at end of file diff --git a/healthcare/healthcare/doctype/observation/observation.html b/healthcare/healthcare/doctype/observation/observation.html new file mode 100644 index 0000000000..178a767a94 --- /dev/null +++ b/healthcare/healthcare/doctype/observation/observation.html @@ -0,0 +1,360 @@ + + + +
+
+
+
Referred By:
+ {%- if doc.practitioner_name -%} +
{{ doc.practitioner_name.upper() }}
+ {% else %} +
Self
+ {% endif %} +
+ {%- if doc.sales_invoice -%} +
+
Invoice No.:
+
{{ doc.sales_invoice }}
+
+ {% endif %} +
+
+
+
+
+ + SAMPLE + +
+
+ Collected on +
+
+
+
+ + INVESTIGATION + +
+
+ Method +
+
+
+
+ + RESULT + +
+
+ Reported on +
+
+
+
+ + UNIT + +
+
+
+
+ + REFERENCE INTERVAL + +
+
+
+ +{% if doc.get("name") %} + {% set full_data = get_observations_for_medical_record(doc.name, doc.parent_observation) %} +
+ {% for data in full_data[0] %} + {% if not data.get("has_component") %} + {% if data.get("observation").get("preferred_display_name") %} + {% set observation_name = data.get("observation").get("preferred_display_name") %} + {% else %} + {% set observation_name = data.get("observation").get("observation_template") %} + {% endif %} + + {% if data.get("observation") or data.get("observation") %} + {% if data.get("observation").get("status")=="Approved" and (data.get("observation").get("result_data") or data.get("observation").get("result_text") or data.get("observation").get("result_select") not in [None, "", "Null"]) %} +
+
+ + {{observation_name}} + +
+
+
+
+ {% if data.get("observation").get("sample") %} +
+ {{data.get("observation").get("sample")}} +
+ {% else %} +
+ {{ frappe.db.get_value("Observation Template", data.get("observation").get("observation_template"), "sample") or "" }} +
+ {% endif %} + {% if data.get("observation").get("received_time") %} +
+ {{frappe.utils.format_datetime(data.get("observation").get("received_time"))[:-3]}} +
+ {% endif %} +
+
+
+ {{observation_name}} +
+ {% if data.get("observation").get("method") %} +
+ {{data.get("observation").get("method")}} +
+ {% endif %} +
+
+
+ {% if data.get("observation").get("result_data") or data.get("observation").get("result_select") %} + {{data.get("observation").get("result_data") + or data.get("observation").get("result_select")}} + {% elif data.get("observation").get("result_text") %} + {% if '
' in data.get("observation").get("result_text") %} + {% if data.get("observation").get("result_text")|length <= 60 %} + {{data.get("observation").get("result_text")}} + {% endif %} + {% elif data.get("observation").get("result_text")|length <= 24 %} + {{data.get("observation").get("result_text")}} + {% endif %} + {% endif %} +
+ {% if data.get("observation").get("time_of_result") %} +
+ {{frappe.utils.format_datetime(data.get("observation").get("time_of_result"))[:-3]}} +
+ {% endif %} +
+
+ {% if data.get("observation").get("permitted_unit") %} + {{data.get("observation").get("permitted_unit")}} + {% endif %} +
+
+ {% if data.get("observation").get("reference") %} + {{data.get("observation").get("reference")}} + {% endif %} +
+
+ {% if data.get("observation").get("result_text") %} + {% if '
' in data.get("observation").get("result_text") %} + {% if data.get("observation").get("result_text")|length > 60 %} +
+ {{data.get("observation").get("result_text")}} +
+ {% endif %} + {% elif data.get("observation").get("result_text")|length > 24 %} +
+ {{data.get("observation").get("result_text")}} +
+ {% endif %} + {% endif %} + {% if data.get("observation").get("result_interpretation") %} +
+ {{data.get("observation").get("result_interpretation")}} +
+ {% endif %} + {% if data.get("observation").get("note") %} +
+ {{data.get("observation").get("note")}} +
+ {% endif %} + {% if data.get("observation").get("description") %} +
+ {{data.get("observation").get("description")}} +
+ {% endif %} +
+ + {% endif %} + {% endif %} + {% else %} + {% if data["obs_approved"] and data[data.get("observation")] and data["has_result"] %} +
+
+
+
+
+ + {{data.get("display_name")}} + +
+ {% for comps in data[data.get("observation")] %} + {% if comps.get("observation").get("preferred_display_name") %} + {% set observation_name = comps.get("observation").get("preferred_display_name") %} + {% else %} + {% set observation_name = comps.get("observation").get("observation_template") %} + {% endif %} + {% if comps.get("observation").get("status")=="Approved" and comps.get("observation") %} + {% if comps.get("observation").get("result_data") or comps.get("observation").get("result_text") or comps.get("observation").get("result_select") not in [None, "", "Null"] %} +
+
+
+
+ {% if comps.get("observation").get("sample") %} +
+ {{comps.get("observation").get("sample")}} +
+ {% else %} +
+ {{ frappe.db.get_value("Observation Template", comps.get("observation").get("observation_template"), "sample") or ""}} +
+ {% endif %} + {% if comps.get("observation").get("received_time") %} +
+ {{frappe.utils.format_datetime(comps.get("observation").get("received_time"))[:-3]}} +
+ {% endif %} +
+
+
+ {{observation_name}} +
+ {% if comps.get("observation").get("method") %} +
+ {{comps.get("observation").get("method")}} +
+ {% endif %} +
+
+
+ {% if comps.get("observation").get("result_data") or comps.get("observation").get("result_select") %} + {{comps.get("observation").get("result_data") + or comps.get("observation").get("result_select")}} + {% elif comps.get("observation").get("result_text") %} + {% if '
' in comps.get("observation").get("result_text") %} + {% if comps.get("observation").get("result_text")|length <= 60 %} + {{comps.get("observation").get("result_text")}} + {% endif %} + {% elif comps.get("observation").get("result_text")|length <= 24 %} + {{comps.get("observation").get("result_text")}} + {% endif %} + {% endif %} +
+ {% if comps.get("observation").get("time_of_result") %} +
+ {{frappe.utils.format_datetime(comps.get("observation").get("time_of_result"))[:-3]}} +
+ {% endif %} +
+
+ {% if comps.get("observation").get("permitted_unit") %} + {{comps.get("observation").get("permitted_unit")}} + {% endif %} +
+
+ {% if comps.get("observation").get("reference") %} + {{comps.get("observation").get("reference")}} + {% endif %} +
+
+ {% if comps.get("observation").get("result_text") %} + {% if '
' in comps.get("observation").get("result_text") %} + {% if comps.get("observation").get("result_text")|length > 60 %} +
+ {{comps.get("observation").get("result_text")}} +
+ {% endif %} + {% elif comps.get("observation").get("result_text")|length > 24 %} +
+ {{comps.get("observation").get("result_text")}} +
+ {% endif %} + {% endif %} + {% if comps.get("observation").get("result_interpretation") %} +
+ {{comps.get("observation").get("result_interpretation")}} +
+ {% endif %} + {% if comps.get("observation").get("note") %} +
+ {{comps.get("observation").get("note")}} +
+ {% endif %} + {% if comps.get("observation").get("description") and not data.get("description") %} +
+ {{comps.get("observation").get("description")}} +
+ {% endif %} +
+
+ {% endif %} + {% endif %} + {% endfor %} +
+ {{data.get("description") or ""}} +
+
+
+ + + {% endif %} + {% endif %} + {% endfor %} + +{% endif %} \ No newline at end of file diff --git a/healthcare/healthcare/doctype/observation/observation.py b/healthcare/healthcare/doctype/observation/observation.py index ed8782c53d..ed5611a538 100644 --- a/healthcare/healthcare/doctype/observation/observation.py +++ b/healthcare/healthcare/doctype/observation/observation.py @@ -602,3 +602,17 @@ def eval_condition_and_formula(d, data):

Hint: {4}""" ).format(d.parenttype, get_link_to_form(d.parenttype, d.parent), d.idx, err, description) frappe.throw(message, title=_("Error in formula")) + + +def get_observations_for_medical_record(observation, parent_observation=None): + if not observation: + return + + if parent_observation: + obs_doc = frappe.get_doc("Observation", parent_observation) + else: + obs_doc = frappe.get_doc("Observation", observation) + + out_data, obs_length = aggregate_and_return_observation_data([obs_doc]) + + return out_data, obs_length diff --git a/healthcare/healthcare/doctype/patient_history_settings/patient_history_settings.py b/healthcare/healthcare/doctype/patient_history_settings/patient_history_settings.py index 79ee6b41aa..ad31492780 100644 --- a/healthcare/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/healthcare/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -82,29 +82,23 @@ def create_medical_record(doc, method=None): reference = doc.name if doc.doctype == "Observation": - if doc.reference_docname or doc.sales_invoice: - ref_docname = doc.reference_docname - if doc.sales_invoice: - ref_docname = doc.sales_invoice - - reference = frappe.db.exists("Diagnostic Report", {"docname": ref_docname}) + if doc.parent_observation: + reference = doc.parent_observation if frappe.db.exists("Patient Medical Record", {"reference_name": reference}): if doc.doctype == "Observation" and reference: update_medical_record(doc, reference=reference) return - subject = set_subject_field(doc, reference) + subject = set_subject_field(doc) date_field = get_date_field(doc.doctype) medical_record = frappe.new_doc("Patient Medical Record") medical_record.patient = doc.patient medical_record.subject = subject medical_record.status = "Open" medical_record.communication_date = doc.get(date_field) - medical_record.reference_doctype = ( - doc.doctype if doc.doctype != "Observation" else "Diagnostic Report" - ) - medical_record.reference_name = doc.name if doc.doctype != "Observation" else reference + medical_record.reference_doctype = doc.doctype + medical_record.reference_name = reference medical_record.reference_owner = doc.owner medical_record.save(ignore_permissions=True) @@ -120,7 +114,7 @@ def update_medical_record(doc, method=None, reference=None): ) if medical_record_id: - subject = set_subject_field(doc, reference) + subject = set_subject_field(doc) frappe.db.set_value("Patient Medical Record", medical_record_id, "subject", subject) else: create_medical_record(doc) @@ -136,18 +130,14 @@ def delete_medical_record(doc, method=None): frappe.delete_doc("Patient Medical Record", record, force=1) -def set_subject_field(doc, reference=None): +def set_subject_field(doc): meta = frappe.get_meta(doc.doctype) subject = "" patient_history_fields = get_patient_history_fields(doc) if doc.doctype == "Observation": - if doc.reference_docname or doc.sales_invoice: - doc = frappe.get_doc("Diagnostic Report", reference) - subject = frappe.render_template( - "healthcare/healthcare/doctype/diagnostic_report/diagnostic_report.html", dict(doc=doc) - ) - else: - return + subject = frappe.render_template( + "healthcare/healthcare/doctype/observation/observation.html", dict(doc=doc) + ) else: for entry in patient_history_fields: fieldname = entry.get("fieldname") diff --git a/healthcare/hooks.py b/healthcare/hooks.py index 7d8909318b..88d8282d27 100644 --- a/healthcare/hooks.py +++ b/healthcare/hooks.py @@ -72,6 +72,7 @@ "methods": [ "healthcare.healthcare.doctype.diagnostic_report.diagnostic_report.diagnostic_report_print", "healthcare.healthcare.utils.generate_barcodes", + "healthcare.healthcare.doctype.observation.observation.get_observations_for_medical_record", ] } diff --git a/healthcare/patches/v15_0/create_patient_medical_records_for_observations.py b/healthcare/patches/v15_0/create_patient_medical_records_for_observations.py index e5691d40c0..2f4bf952b8 100644 --- a/healthcare/patches/v15_0/create_patient_medical_records_for_observations.py +++ b/healthcare/patches/v15_0/create_patient_medical_records_for_observations.py @@ -1,21 +1,34 @@ import frappe +from frappe.utils import getdate def execute(): - diagnostic_report = frappe.db.get_all("Diagnostic Report", pluck="name") + observations = frappe.db.get_all("Observation", filters={"docstatus": 1}, pluck="name") - for diag in diagnostic_report: - diag_doc = frappe.get_doc("Diagnostic Report", diag) + for obs in observations: + obs_doc = frappe.get_doc("Observation", obs) subject = frappe.render_template( - "healthcare/healthcare/doctype/diagnostic_report/diagnostic_report.html", dict(doc=diag_doc) + "healthcare/healthcare/doctype/observation/observation.html", dict(doc=obs_doc) ) - medical_record = frappe.new_doc("Patient Medical Record") - medical_record.patient = diag_doc.patient - medical_record.subject = subject - medical_record.status = "Open" - medical_record.communication_date = diag_doc.reference_posting_date - medical_record.reference_doctype = "Diagnostic Report" - medical_record.reference_name = diag_doc.name - medical_record.reference_owner = diag_doc.owner - medical_record.save(ignore_permissions=True) + + reference = obs + if obs_doc.parent_observation: + reference = obs_doc.parent_observation + + exists = frappe.db.exists( + "Patient Medical Record", {"reference_doctype": "Observation", "reference_name": reference} + ) + + if exists: + frappe.db.set_value("Patient Medical Record", exists, "subject", subject) + else: + medical_record = frappe.new_doc("Patient Medical Record") + medical_record.patient = obs_doc.patient + medical_record.subject = subject + medical_record.status = "Open" + medical_record.communication_date = getdate(obs_doc.modified) + medical_record.reference_doctype = "Observation" + medical_record.reference_name = reference + medical_record.reference_owner = obs_doc.owner + medical_record.save(ignore_permissions=True) From bd7ba829bb9c62edafbd592785ce098a43eaf7db Mon Sep 17 00:00:00 2001 From: Sajin SR Date: Fri, 20 Dec 2024 18:44:12 +0530 Subject: [PATCH 18/18] fix(Sample Collection): set sales invoice reference conditionally --- .../healthcare/doctype/sample_collection/sample_collection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/healthcare/healthcare/doctype/sample_collection/sample_collection.py b/healthcare/healthcare/doctype/sample_collection/sample_collection.py index 5d9fdf25b0..9efadf848a 100644 --- a/healthcare/healthcare/doctype/sample_collection/sample_collection.py +++ b/healthcare/healthcare/doctype/sample_collection/sample_collection.py @@ -116,7 +116,9 @@ def insert_observation(selected, sample_collection, component_observations=None, specimen=comp_obs_ref.get(obs.get("name")) or comp_obs_ref.get(i + 1) or comp_obs_ref.get(obs.get("idx")), - invoice=sample_col_doc.get("reference_name"), + invoice=sample_col_doc.get("reference_name") + if sample_col_doc.reference_doc == "Sales Invoice" + else None, practitioner=sample_col_doc.get("referring_practitioner"), child=obs.get("reference_child") if obs.get("reference_child") else "", service_request=obs.get("service_request"),