diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 455ab861f9..ac623e947d 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -4,11 +4,7 @@ set -e cd ~ || exit -sudo apt-get install redis-server - -sudo apt install nodejs - -sudo apt install npm +sudo apt-get install redis-server libcups2-dev pip install frappe-bench @@ -32,7 +28,6 @@ wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/w tar -xf /tmp/wkhtmltox.tar.xz -C /tmp sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf sudo chmod o+x /usr/local/bin/wkhtmltopdf -sudo apt-get install libcups2-dev cd ~/frappe-bench || exit diff --git a/.github/helper/semgrep_rules/report.py b/.github/helper/semgrep_rules/report.py new file mode 100644 index 0000000000..ff278408e1 --- /dev/null +++ b/.github/helper/semgrep_rules/report.py @@ -0,0 +1,15 @@ +from frappe import _ + + +# ruleid: frappe-missing-translate-function-in-report-python +{"label": "Field Label"} + +# ruleid: frappe-missing-translate-function-in-report-python +dict(label="Field Label") + + +# ok: frappe-missing-translate-function-in-report-python +{"label": _("Field Label")} + +# ok: frappe-missing-translate-function-in-report-python +dict(label=_("Field Label")) diff --git a/.github/helper/semgrep_rules/report.yml b/.github/helper/semgrep_rules/report.yml new file mode 100644 index 0000000000..7f3dd011dc --- /dev/null +++ b/.github/helper/semgrep_rules/report.yml @@ -0,0 +1,21 @@ +rules: +- id: frappe-missing-translate-function-in-report-python + paths: + include: + - "**/report" + exclude: + - "**/regional" + pattern-either: + - patterns: + - pattern: | + {..., "label": "...", ...} + - pattern-not: | + {..., "label": _("..."), ...} + - patterns: + - pattern: dict(..., label="...", ...) + - pattern-not: dict(..., label=_("..."), ...) + message: | + All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations + languages: [python] + severity: ERROR + diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 72d4028ce6..92a19621d1 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -7,10 +7,13 @@ on: - '**.md' workflow_dispatch: +concurrency: + group: patch-develop-${{ github.event.number }} + cancel-in-progress: true jobs: test: - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest timeout-minutes: 60 name: Patch Test @@ -31,7 +34,13 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.6 + python-version: 3.7 + + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 14 + check-latest: true - name: Add to Hosts run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 3a1ecd399c..71e9c2cd13 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -12,9 +12,13 @@ on: - '**.js' - '**.md' +concurrency: + group: server-develop-${{ github.event.number }} + cancel-in-progress: true + jobs: test: - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest timeout-minutes: 60 strategy: @@ -43,6 +47,12 @@ jobs: with: python-version: 3.7 + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 14 + check-latest: true + - name: Add to Hosts run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts @@ -107,7 +117,7 @@ jobs: name: Coverage Wrap Up needs: test container: python:3-slim - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - name: Clone uses: actions/checkout@v2 diff --git a/.github/workflows/translation_linter.yml b/.github/workflows/translation_linter.yml deleted file mode 100644 index 4becaebd6b..0000000000 --- a/.github/workflows/translation_linter.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Frappe Linter -on: - pull_request: - branches: - - develop - - version-12-hotfix - - version-11-hotfix -jobs: - check_translation: - name: Translation Syntax Check - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@v2 - - name: Setup python3 - uses: actions/setup-python@v1 - with: - python-version: 3.6 - - name: Validating Translation Syntax - run: | - git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q - files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) - python $GITHUB_WORKSPACE/.github/helper/translation.py $files diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 0be9bd8f87..658892c20e 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -6,9 +6,13 @@ on: - '**.md' workflow_dispatch: +concurrency: + group: ui-develop-${{ github.event.number }} + cancel-in-progress: true + jobs: test: - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest timeout-minutes: 60 strategy: @@ -95,7 +99,7 @@ jobs: run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests - name: cypress pre-requisites - run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 --no-lockfile + run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 @testing-library/cypress@^8 --no-lockfile - name: Build Assets diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js index 820a23a207..39b00d3263 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -2,8 +2,11 @@ context('Organizational Chart', () => { before(() => { cy.login(); cy.visit('/app/website'); + }); + + it('navigates to org chart', () => { + cy.visit('/app'); cy.awesomebar('Organizational Chart'); - cy.wait(500); cy.url().should('include', '/organizational-chart'); cy.window().its('frappe.csrf_token').then(csrf_token => { diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js index df90dbfa22..6e75151396 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -1,9 +1,14 @@ context('Organizational Chart Mobile', () => { before(() => { cy.login(); - cy.viewport(375, 667); cy.visit('/app/website'); + }); + + it('navigates to org chart', () => { + cy.viewport(375, 667); + cy.visit('/app'); cy.awesomebar('Organizational Chart'); + cy.url().should('include', '/organizational-chart'); cy.window().its('frappe.csrf_token').then(csrf_token => { return cy.request({ diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py index c417a493c6..834227bb58 100644 --- a/erpnext/accounts/custom/address.py +++ b/erpnext/accounts/custom/address.py @@ -1,7 +1,7 @@ import frappe from frappe import _ from frappe.contacts.doctype.address.address import Address -from frappe.contacts.doctype.address.address import get_address_templates +from frappe.contacts.doctype.address.address import get_address_templates, get_address_display class ERPNextAddress(Address): def validate(self): @@ -22,6 +22,16 @@ class ERPNextAddress(Address): frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table."), title=_("Company Not Linked")) + def on_update(self): + """ + After Address is updated, update the related 'Primary Address' on Customer. + """ + address_display = get_address_display(self.as_dict()) + filters = { "customer_primary_address": self.name } + customers = frappe.db.get_all("Customer", filters=filters, as_list=True) + for customer_name in customers: + frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display) + @frappe.whitelist() def get_shipping_address(company, address = None): filters = [ diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js index f7f1a5fb15..7a1d735948 100644 --- a/erpnext/accounts/doctype/account/account.js +++ b/erpnext/accounts/doctype/account/account.js @@ -74,7 +74,7 @@ frappe.ui.form.on('Account', { }); } else if (cint(frm.doc.is_group) == 0 && frappe.boot.user.can_read.indexOf("GL Entry") !== -1) { - cur_frm.add_custom_button(__('Ledger'), function () { + frm.add_custom_button(__('Ledger'), function () { frappe.route_options = { "account": frm.doc.name, "from_date": frappe.sys_defaults.year_start_date, diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.js b/erpnext/accounts/doctype/accounting_period/test_accounting_period.js deleted file mode 100644 index 71ce5b8d04..0000000000 --- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Accounting Period", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Accounting Period - () => frappe.tests.make('Accounting Period', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js index e44af3a916..0627675de7 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js @@ -6,46 +6,3 @@ frappe.ui.form.on('Accounts Settings', { } }); - -frappe.tour['Accounts Settings'] = [ - { - fieldname: "acc_frozen_upto", - title: "Accounts Frozen Upto", - description: __("Freeze accounting transactions up to specified date, nobody can make/modify entry except the specified Role."), - }, - { - fieldname: "frozen_accounts_modifier", - title: "Role Allowed to Set Frozen Accounts & Edit Frozen Entries", - description: __("Users with this Role are allowed to set frozen accounts and create/modify accounting entries against frozen accounts.") - }, - { - fieldname: "determine_address_tax_category_from", - title: "Determine Address Tax Category From", - description: __("Tax category can be set on Addresses. An address can be Shipping or Billing address. Set which addres to select when applying Tax Category.") - }, - { - fieldname: "over_billing_allowance", - title: "Over Billing Allowance Percentage", - description: __("The percentage by which you can overbill transactions. For example, if the order value is $100 for an Item and percentage here is set as 10% then you are allowed to bill for $110.") - }, - { - fieldname: "credit_controller", - title: "Credit Controller", - description: __("Select the role that is allowed to submit transactions that exceed credit limits set. The credit limit can be set in the Customer form.") - }, - { - fieldname: "make_payment_via_journal_entry", - title: "Make Payment via Journal Entry", - description: __("When checked, if user proceeds to make payment from an invoice, the system will open a Journal Entry instead of a Payment Entry.") - }, - { - fieldname: "unlink_payment_on_cancellation_of_invoice", - title: "Unlink Payment on Cancellation of Invoice", - description: __("If checked, system will unlink the payment against the respective invoice.") - }, - { - fieldname: "unlink_advance_payment_on_cancelation_of_order", - title: "Unlink Advance Payment on Cancellation of Order", - description: __("Similar to the previous option, this unlinks any advance payments made against Purchase/Sales Orders.") - } -]; diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index a246ae51a4..7d0ecfbafd 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -19,6 +19,7 @@ "delete_linked_ledger_entries", "book_asset_depreciation_entry_automatically", "unlink_advance_payment_on_cancelation_of_order", + "enable_common_party_accounting", "post_change_gl_entries", "enable_discount_accounting", "tax_settings_section", @@ -268,6 +269,12 @@ "fieldname": "enable_discount_accounting", "fieldtype": "Check", "label": "Enable Discount Accounting" + }, + { + "default": "0", + "fieldname": "enable_common_party_accounting", + "fieldtype": "Check", + "label": "Enable Common Party Accounting" } ], "icon": "icon-cog", @@ -275,7 +282,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-08-09 13:08:04.335416", + "modified": "2021-08-19 11:17:38.788054", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/bank/test_bank.js b/erpnext/accounts/doctype/bank/test_bank.js deleted file mode 100644 index 9ec264415a..0000000000 --- a/erpnext/accounts/doctype/bank/test_bank.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Bank", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Bank - () => frappe.tests.make('Bank', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/bank_account/test_bank_account.js b/erpnext/accounts/doctype/bank_account/test_bank_account.js deleted file mode 100644 index c20a7990e8..0000000000 --- a/erpnext/accounts/doctype/bank_account/test_bank_account.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Bank Account", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Bank Account - () => frappe.tests.make('Bank Account', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/bank_account_subtype/test_bank_account_subtype.js b/erpnext/accounts/doctype/bank_account_subtype/test_bank_account_subtype.js deleted file mode 100644 index f59999845a..0000000000 --- a/erpnext/accounts/doctype/bank_account_subtype/test_bank_account_subtype.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Bank Account Subtype", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Bank Account Subtype - () => frappe.tests.make('Bank Account Subtype', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/bank_guarantee/test_bank_guarantee.js b/erpnext/accounts/doctype/bank_guarantee/test_bank_guarantee.js deleted file mode 100644 index 0c60920cf9..0000000000 --- a/erpnext/accounts/doctype/bank_guarantee/test_bank_guarantee.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Bank Guarantee", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Bank Guarantee - () => frappe.tests.make('Bank Guarantee', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.js b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.js deleted file mode 100644 index 305119e137..0000000000 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Bank Transaction", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Bank Transaction - () => frappe.tests.make('Bank Transaction', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/cash_flow_mapper/test_cash_flow_mapper.js b/erpnext/accounts/doctype/cash_flow_mapper/test_cash_flow_mapper.js deleted file mode 100644 index 12ca254c5a..0000000000 --- a/erpnext/accounts/doctype/cash_flow_mapper/test_cash_flow_mapper.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Cash Flow Mapper", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Cash Flow Mapper - () => frappe.tests.make('Cash Flow Mapper', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.js b/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.js deleted file mode 100644 index 1970ca806d..0000000000 --- a/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Cash Flow Mapping", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Cash Flow Mapping - () => frappe.tests.make('Cash Flow Mapping', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/cash_flow_mapping_template/test_cash_flow_mapping_template.js b/erpnext/accounts/doctype/cash_flow_mapping_template/test_cash_flow_mapping_template.js deleted file mode 100644 index 12546ce2e3..0000000000 --- a/erpnext/accounts/doctype/cash_flow_mapping_template/test_cash_flow_mapping_template.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Cash Flow Mapping Template", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Cash Flow Mapping Template - () => frappe.tests.make('Cash Flow Mapping Template', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/cash_flow_mapping_template_details/test_cash_flow_mapping_template_details.js b/erpnext/accounts/doctype/cash_flow_mapping_template_details/test_cash_flow_mapping_template_details.js deleted file mode 100644 index eecabda751..0000000000 --- a/erpnext/accounts/doctype/cash_flow_mapping_template_details/test_cash_flow_mapping_template_details.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Cash Flow Mapping Template Details", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Cash Flow Mapping Template Details - () => frappe.tests.make('Cash Flow Mapping Template Details', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/cashier_closing/test_cashier_closing.js b/erpnext/accounts/doctype/cashier_closing/test_cashier_closing.js deleted file mode 100644 index a7fcc8d842..0000000000 --- a/erpnext/accounts/doctype/cashier_closing/test_cashier_closing.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Cashier Closing", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Cashier Closing - () => frappe.tests.make('Cashier Closing', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/test_chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/test_chart_of_accounts_importer.js deleted file mode 100644 index b075a015d6..0000000000 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/test_chart_of_accounts_importer.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Chart of Accounts Importer", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Chart of Accounts Importer - () => frappe.tests.make('Chart of Accounts Importer', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.js b/erpnext/accounts/doctype/coupon_code/test_coupon_code.js deleted file mode 100644 index 460fedc97f..0000000000 --- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Coupon Code", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Coupon Code - () => frappe.tests.make('Coupon Code', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.js deleted file mode 100644 index 57c6a7871d..0000000000 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Exchange Rate Revaluation", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Exchange Rate Revaluation - () => frappe.tests.make('Exchange Rate Revaluation', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/finance_book/test_finance_book.js b/erpnext/accounts/doctype/finance_book/test_finance_book.js deleted file mode 100644 index 9fb7d4fcc8..0000000000 --- a/erpnext/accounts/doctype/finance_book/test_finance_book.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Finance Book", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Finance Book - () => frappe.tests.make('Finance Book', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/gl_entry/test_gl_entry.js b/erpnext/accounts/doctype/gl_entry/test_gl_entry.js deleted file mode 100644 index 2986e5e4e3..0000000000 --- a/erpnext/accounts/doctype/gl_entry/test_gl_entry.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: GL Entry", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('GL Entry', [ - // insert a new GL Entry - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/item_tax_template/test_item_tax_template.js b/erpnext/accounts/doctype/item_tax_template/test_item_tax_template.js deleted file mode 100644 index 6893499391..0000000000 --- a/erpnext/accounts/doctype/item_tax_template/test_item_tax_template.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Item Tax Template", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Item Tax Template - () => frappe.tests.make('Item Tax Template', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 937597bc55..dc341d7ad7 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -66,6 +66,7 @@ class JournalEntry(AccountsController): self.update_expense_claim() self.update_inter_company_jv() self.update_invoice_discounting() + self.update_status_for_full_and_final_statement() check_if_stock_and_account_balance_synced(self.posting_date, self.company, self.doctype, self.name) @@ -83,6 +84,7 @@ class JournalEntry(AccountsController): self.unlink_inter_company_jv() self.unlink_asset_adjustment_entry() self.update_invoice_discounting() + self.update_status_for_full_and_final_statement() def get_title(self): return self.pay_to_recd_from or self.accounts[0].account @@ -98,6 +100,15 @@ class JournalEntry(AccountsController): for voucher_no in list(set(order_list)): frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid() + def update_status_for_full_and_final_statement(self): + for entry in self.accounts: + if entry.reference_type == "Full and Final Statement": + if self.docstatus == 1: + frappe.db.set_value("Full and Final Statement", entry.reference_name, "status", "Paid") + elif self.docstatus == 2: + frappe.db.set_value("Full and Final Statement", entry.reference_name, "status", "Unpaid") + + def validate_inter_company_accounts(self): if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference) @@ -643,7 +654,10 @@ class JournalEntry(AccountsController): for d in self.accounts: if d.reference_type=="Expense Claim" and d.reference_name: doc = frappe.get_doc("Expense Claim", d.reference_name) - update_reimbursed_amount(doc, jv=self.name) + if self.docstatus == 2: + update_reimbursed_amount(doc, -1 * d.debit) + else: + update_reimbursed_amount(doc, d.debit) def validate_expense_claim(self): diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index a89fefde07..dff883aef9 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -202,7 +202,7 @@ "fieldname": "reference_type", "fieldtype": "Select", "label": "Reference Type", - "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees" + "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement" }, { "fieldname": "reference_name", @@ -280,7 +280,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-06-26 14:06:54.833738", + "modified": "2021-08-30 21:27:32.200299", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", diff --git a/erpnext/accounts/doctype/loyalty_point_entry/test_loyalty_point_entry.js b/erpnext/accounts/doctype/loyalty_point_entry/test_loyalty_point_entry.js deleted file mode 100644 index a916b67522..0000000000 --- a/erpnext/accounts/doctype/loyalty_point_entry/test_loyalty_point_entry.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Loyalty Point Entry", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Loyalty Point Entry - () => frappe.tests.make('Loyalty Point Entry', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.js deleted file mode 100644 index 9321c14e1f..0000000000 --- a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Loyalty Program", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Loyalty Program - () => frappe.tests.make('Loyalty Program', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.js deleted file mode 100644 index f95d0d8213..0000000000 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Opening Invoice Creation Tool", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Opening Invoice Creation Tool - () => frappe.tests.make('Opening Invoice Creation Tool', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/hr/doctype/employee_onboarding_activity/__init__.py b/erpnext/accounts/doctype/party_link/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_onboarding_activity/__init__.py rename to erpnext/accounts/doctype/party_link/__init__.py diff --git a/erpnext/accounts/doctype/party_link/party_link.js b/erpnext/accounts/doctype/party_link/party_link.js new file mode 100644 index 0000000000..6da9291d64 --- /dev/null +++ b/erpnext/accounts/doctype/party_link/party_link.js @@ -0,0 +1,33 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Party Link', { + refresh: function(frm) { + frm.set_query('primary_role', () => { + return { + filters: { + name: ['in', ['Customer', 'Supplier']] + } + }; + }); + + frm.set_query('secondary_role', () => { + let party_types = Object.keys(frappe.boot.party_account_types) + .filter(p => p != frm.doc.primary_role); + return { + filters: { + name: ['in', party_types] + } + }; + }); + }, + + primary_role(frm) { + frm.set_value('primary_party', ''); + frm.set_value('secondary_role', ''); + }, + + secondary_role(frm) { + frm.set_value('secondary_party', ''); + } +}); diff --git a/erpnext/accounts/doctype/party_link/party_link.json b/erpnext/accounts/doctype/party_link/party_link.json new file mode 100644 index 0000000000..a1bb15f0d6 --- /dev/null +++ b/erpnext/accounts/doctype/party_link/party_link.json @@ -0,0 +1,102 @@ +{ + "actions": [], + "autoname": "ACC-PT-LNK-.###.", + "creation": "2021-08-18 21:06:53.027695", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "primary_role", + "secondary_role", + "column_break_2", + "primary_party", + "secondary_party" + ], + "fields": [ + { + "fieldname": "primary_role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Primary Role", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "depends_on": "primary_role", + "fieldname": "secondary_role", + "fieldtype": "Link", + "label": "Secondary Role", + "mandatory_depends_on": "primary_role", + "options": "DocType" + }, + { + "depends_on": "primary_role", + "fieldname": "primary_party", + "fieldtype": "Dynamic Link", + "label": "Primary Party", + "mandatory_depends_on": "primary_role", + "options": "primary_role" + }, + { + "depends_on": "secondary_role", + "fieldname": "secondary_party", + "fieldtype": "Dynamic Link", + "label": "Secondary Party", + "mandatory_depends_on": "secondary_role", + "options": "secondary_role" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-08-25 20:08:56.761150", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Party Link", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "primary_party", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py new file mode 100644 index 0000000000..7d58506ce7 --- /dev/null +++ b/erpnext/accounts/doctype/party_link/party_link.py @@ -0,0 +1,26 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + +class PartyLink(Document): + def validate(self): + if self.primary_role not in ['Customer', 'Supplier']: + frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."), + title=_("Invalid Primary Role")) + + existing_party_link = frappe.get_all('Party Link', { + 'primary_party': self.secondary_party + }, pluck="primary_role") + if existing_party_link: + frappe.throw(_('{} {} is already linked with another {}') + .format(self.secondary_role, self.secondary_party, existing_party_link[0])) + + existing_party_link = frappe.get_all('Party Link', { + 'secondary_party': self.primary_party + }, pluck="primary_role") + if existing_party_link: + frappe.throw(_('{} {} is already linked with another {}') + .format(self.primary_role, self.primary_party, existing_party_link[0])) diff --git a/erpnext/accounts/doctype/party_link/test_party_link.py b/erpnext/accounts/doctype/party_link/test_party_link.py new file mode 100644 index 0000000000..a3ea3959ba --- /dev/null +++ b/erpnext/accounts/doctype/party_link/test_party_link.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestPartyLink(unittest.TestCase): + pass diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index d96bc271ef..3be3925b5a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -872,7 +872,7 @@ frappe.ui.form.on('Payment Entry', { && frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions && frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) { unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges - + frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate; + - frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate; } else if (frm.doc.payment_type == "Pay" && frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions && frm.doc.total_allocated_amount < frm.doc.received_amount + (total_deductions / frm.doc.target_exchange_rate)) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index abacee985c..a5fcad4996 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -75,9 +75,9 @@ class PaymentEntry(AccountsController): if self.difference_amount: frappe.throw(_("Difference Amount must be zero")) self.make_gl_entries() + self.update_expense_claim() self.update_outstanding_amounts() self.update_advance_paid() - self.update_expense_claim() self.update_donation() self.update_payment_schedule() self.set_status() @@ -85,9 +85,9 @@ class PaymentEntry(AccountsController): def on_cancel(self): self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.make_gl_entries(cancel=1) + self.update_expense_claim() self.update_outstanding_amounts() self.update_advance_paid() - self.update_expense_claim() self.update_donation(cancel=1) self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) @@ -755,9 +755,11 @@ class PaymentEntry(AccountsController): if self.payment_type in ('Pay', 'Internal Transfer'): dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" + rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" against = self.party or self.paid_from elif self.payment_type == 'Receive': dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" + rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" against = self.party or self.paid_to payment_or_advance_account = self.get_party_account_for_taxes() @@ -779,14 +781,13 @@ class PaymentEntry(AccountsController): "cost_center": d.cost_center }, account_currency, item=d)) - #Intentionally use -1 to get net values in party account if not d.included_in_paid_amount or self.advance_tax_account: gl_entries.append( self.get_gl_dict({ "account": payment_or_advance_account, "against": against, - dr_or_cr: -1 * tax_amount, - dr_or_cr + "_in_account_currency": -1 * base_tax_amount + rev_dr_or_cr: tax_amount, + rev_dr_or_cr + "_in_account_currency": base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, @@ -830,7 +831,10 @@ class PaymentEntry(AccountsController): for d in self.get("references"): if d.reference_doctype=="Expense Claim" and d.reference_name: doc = frappe.get_doc("Expense Claim", d.reference_name) - update_reimbursed_amount(doc, self.name) + if self.docstatus == 2: + update_reimbursed_amount(doc, -1 * d.allocated_amount) + else: + update_reimbursed_amount(doc, d.allocated_amount) def update_donation(self, cancel=0): if self.payment_type == "Receive" and self.party_type == "Donor" and self.party: diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.js b/erpnext/accounts/doctype/payment_order/test_payment_order.js deleted file mode 100644 index f63fc54521..0000000000 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Payment Order", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Payment Order - () => frappe.tests.make('Payment Order', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index c71a62dfb3..b1f3e6fd01 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -2,46 +2,10 @@ // For license information, please see license.txt frappe.provide("erpnext.accounts"); - -frappe.ui.form.on("Payment Reconciliation Payment", { - invoice_number: function(frm, cdt, cdn) { - var row = locals[cdt][cdn]; - if(row.invoice_number) { - var parts = row.invoice_number.split(' | '); - var invoice_type = parts[0]; - var invoice_number = parts[1]; - - var invoice_amount = frm.doc.invoices.filter(function(d) { - return d.invoice_type === invoice_type && d.invoice_number === invoice_number; - })[0].outstanding_amount; - - frappe.model.set_value(cdt, cdn, "allocated_amount", Math.min(invoice_amount, row.amount)); - - frm.call({ - doc: frm.doc, - method: 'get_difference_amount', - args: { - child_row: row - }, - callback: function(r, rt) { - if(r.message) { - frappe.model.set_value(cdt, cdn, - "difference_amount", r.message); - } - } - }); - } - } -}); - erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationController extends frappe.ui.form.Controller { onload() { var me = this; - this.frm.set_query("party", function() { - check_mandatory(me.frm); - }); - this.frm.set_query("party_type", function() { return { "filters": { @@ -88,15 +52,36 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo refresh() { this.frm.disable_save(); - this.toggle_primary_action(); + + if (this.frm.doc.receivable_payable_account) { + this.frm.add_custom_button(__('Get Unreconciled Entries'), () => + this.frm.trigger("get_unreconciled_entries") + ); + } + if (this.frm.doc.invoices.length && this.frm.doc.payments.length) { + this.frm.add_custom_button(__('Allocate'), () => + this.frm.trigger("allocate") + ); + } + if (this.frm.doc.allocation.length) { + this.frm.add_custom_button(__('Reconcile'), () => + this.frm.trigger("reconcile") + ); + } } - onload_post_render() { - this.toggle_primary_action(); + company() { + var me = this; + this.frm.set_value('receivable_payable_account', ''); + me.frm.clear_table("allocation"); + me.frm.clear_table("invoices"); + me.frm.clear_table("payments"); + me.frm.refresh_fields(); + me.frm.trigger('party'); } party() { - var me = this + var me = this; if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) { return frappe.call({ method: "erpnext.accounts.party.get_party_account", @@ -109,6 +94,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo if (!r.exc && r.message) { me.frm.set_value("receivable_payable_account", r.message); } + me.frm.refresh(); } }); } @@ -120,16 +106,41 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo doc: me.frm.doc, method: 'get_unreconciled_entries', callback: function(r, rt) { - me.set_invoice_options(); - me.toggle_primary_action(); + if (!(me.frm.doc.payments.length || me.frm.doc.invoices.length)) { + frappe.throw({message: __("No invoice and payment records found for this party")}); + } + me.frm.refresh(); } }); } + allocate() { + var me = this; + let payments = me.frm.fields_dict.payments.grid.get_selected_children(); + if (!(payments.length)) { + payments = me.frm.doc.payments; + } + let invoices = me.frm.fields_dict.invoices.grid.get_selected_children(); + if (!(invoices.length)) { + invoices = me.frm.doc.invoices; + } + return me.frm.call({ + doc: me.frm.doc, + method: 'allocate_entries', + args: { + payments: payments, + invoices: invoices + }, + callback: function() { + me.frm.refresh(); + } + }); + } + reconcile() { var me = this; - var show_dialog = me.frm.doc.payments.filter(d => d.difference_amount && !d.difference_account); + var show_dialog = me.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account); if (show_dialog && show_dialog.length) { @@ -138,7 +149,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo title: __("Select Difference Account"), fields: [ { - fieldname: "payments", fieldtype: "Table", label: __("Payments"), + fieldname: "allocation", fieldtype: "Table", label: __("Allocation"), data: this.data, in_place_edit: true, get_data: () => { return this.data; @@ -179,10 +190,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo }, ], primary_action: function() { - const args = dialog.get_values()["payments"]; + const args = dialog.get_values()["allocation"]; args.forEach(d => { - frappe.model.set_value("Payment Reconciliation Payment", d.docname, + frappe.model.set_value("Payment Reconciliation Allocation", d.docname, "difference_account", d.difference_account); }); @@ -192,9 +203,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo primary_action_label: __('Reconcile Entries') }); - this.frm.doc.payments.forEach(d => { + this.frm.doc.allocation.forEach(d => { if (d.difference_amount && !d.difference_account) { - dialog.fields_dict.payments.df.data.push({ + dialog.fields_dict.allocation.df.data.push({ 'docname': d.name, 'reference_name': d.reference_name, 'difference_amount': d.difference_amount, @@ -203,8 +214,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo } }); - this.data = dialog.fields_dict.payments.df.data; - dialog.fields_dict.payments.grid.refresh(); + this.data = dialog.fields_dict.allocation.df.data; + dialog.fields_dict.allocation.grid.refresh(); dialog.show(); } else { this.reconcile_payment_entries(); @@ -218,48 +229,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo doc: me.frm.doc, method: 'reconcile', callback: function(r, rt) { - me.set_invoice_options(); - me.toggle_primary_action(); + me.frm.clear_table("allocation"); + me.frm.refresh_fields(); + me.frm.refresh(); } }); } - - set_invoice_options() { - var me = this; - var invoices = []; - - $.each(me.frm.doc.invoices || [], function(i, row) { - if (row.invoice_number && !in_list(invoices, row.invoice_number)) - invoices.push(row.invoice_type + " | " + row.invoice_number); - }); - - if (invoices) { - this.frm.fields_dict.payments.grid.update_docfield_property( - 'invoice_number', 'options', "\n" + invoices.join("\n") - ); - - $.each(me.frm.doc.payments || [], function(i, p) { - if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null; - }); - } - - refresh_field("payments"); - } - - toggle_primary_action() { - if ((this.frm.doc.payments || []).length) { - this.frm.fields_dict.reconcile.$input - && this.frm.fields_dict.reconcile.$input.addClass("btn-primary"); - this.frm.fields_dict.get_unreconciled_entries.$input - && this.frm.fields_dict.get_unreconciled_entries.$input.removeClass("btn-primary"); - } else { - this.frm.fields_dict.reconcile.$input - && this.frm.fields_dict.reconcile.$input.removeClass("btn-primary"); - this.frm.fields_dict.get_unreconciled_entries.$input - && this.frm.fields_dict.get_unreconciled_entries.$input.addClass("btn-primary"); - } - } - }; extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm})); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index cfb24c3954..9023b3646f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -1,622 +1,206 @@ { - "allow_copy": 1, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2014-07-09 12:04:51.681583", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_copy": 1, + "creation": "2014-07-09 12:04:51.681583", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "company", + "party_type", + "column_break_4", + "party", + "receivable_payable_account", + "col_break1", + "from_invoice_date", + "to_invoice_date", + "minimum_invoice_amount", + "maximum_invoice_amount", + "invoice_limit", + "column_break_13", + "from_payment_date", + "to_payment_date", + "minimum_payment_amount", + "maximum_payment_amount", + "payment_limit", + "bank_cash_account", + "sec_break1", + "invoices", + "column_break_15", + "payments", + "sec_break2", + "allocation" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "party_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Party Type", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "party_type", + "fieldtype": "Link", + "label": "Party Type", + "options": "DocType", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "party", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Party", - "length": 0, - "no_copy": 0, - "options": "party_type", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.party_type", + "fieldname": "party", + "fieldtype": "Dynamic Link", + "label": "Party", + "options": "party_type", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "receivable_payable_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Receivable / Payable Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.company && doc.party", + "fieldname": "receivable_payable_account", + "fieldtype": "Link", + "label": "Receivable / Payable Account", + "options": "Account", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bank_cash_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Bank / Cash Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "description": "This filter will be applied to Journal Entry.", + "fieldname": "bank_cash_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Bank / Cash Account", + "options": "Account" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "col_break1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "collapsible_depends_on": "eval: doc.invoices.length == 0", + "depends_on": "eval:doc.receivable_payable_account", + "fieldname": "col_break1", + "fieldtype": "Section Break", + "label": "Filters" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "from_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "From Invoice Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:(doc.payments).length || (doc.invoices).length", + "fieldname": "sec_break1", + "fieldtype": "Section Break", + "label": "Unreconciled Entries" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "To Invoice Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "payments", + "fieldtype": "Table", + "label": "Payments", + "options": "Payment Reconciliation Payment" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "minimum_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Minimum Invoice Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "allocation", + "fieldname": "sec_break2", + "fieldtype": "Section Break", + "label": "Allocated Entries" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maximum_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Maximum Invoice Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "invoices", + "fieldtype": "Table", + "label": "Invoices", + "options": "Payment Reconciliation Invoice" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "System will fetch all the entries if limit value is zero.", - "fieldname": "limit", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Limit", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_15", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "get_unreconciled_entries", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Get Unreconciled Entries", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "allocation", + "fieldtype": "Table", + "label": "Allocation", + "options": "Payment Reconciliation Allocation" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sec_break1", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Unreconciled Payment Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "payments", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Payments", - "length": 0, - "no_copy": 0, - "options": "Payment Reconciliation Payment", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "from_invoice_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "From Invoice Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reconcile", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reconcile", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "to_invoice_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "To Invoice Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sec_break2", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Invoice/Journal Entry Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "minimum_invoice_amount", + "fieldtype": "Currency", + "label": "Minimum Invoice Amount" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "invoices", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Invoices", - "length": 0, - "no_copy": 0, - "options": "Payment Reconciliation Invoice", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "description": "System will fetch all the entries if limit value is zero.", + "fieldname": "invoice_limit", + "fieldtype": "Int", + "label": "Invoice Limit" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "fieldname": "from_payment_date", + "fieldtype": "Date", + "label": "From Payment Date" + }, + { + "fieldname": "to_payment_date", + "fieldtype": "Date", + "label": "To Payment Date" + }, + { + "fieldname": "minimum_payment_amount", + "fieldtype": "Currency", + "label": "Minimum Payment Amount" + }, + { + "fieldname": "maximum_payment_amount", + "fieldtype": "Currency", + "label": "Maximum Payment Amount" + }, + { + "fieldname": "payment_limit", + "fieldtype": "Int", + "label": "Payment Limit" + }, + { + "fieldname": "maximum_invoice_amount", + "fieldtype": "Currency", + "label": "Maximum Invoice Amount" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 1, - "icon": "icon-resize-horizontal", - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-01-15 17:42:21.135214", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Payment Reconciliation", - "name_case": "", - "owner": "Administrator", + ], + "hide_toolbar": 1, + "icon": "icon-resize-horizontal", + "issingle": 1, + "links": [], + "modified": "2021-08-30 13:05:51.977861", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Payment Reconciliation", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "read": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "read": 1, + "role": "Accounts User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index acfe1fef2e..1286bf0f0b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe, erpnext -from frappe.utils import flt, today +from frappe.utils import flt, today, getdate, nowdate from frappe import msgprint, _ from frappe.model.document import Document from erpnext.accounts.utils import (get_outstanding_invoices, @@ -27,24 +27,32 @@ class PaymentReconciliation(Document): else: dr_or_cr_notes = [] - self.add_payment_entries(payment_entries + journal_entries + dr_or_cr_notes) + non_reconciled_payments = payment_entries + journal_entries + dr_or_cr_notes + + if self.payment_limit: + non_reconciled_payments = non_reconciled_payments[:self.payment_limit] + + non_reconciled_payments = sorted(non_reconciled_payments, key=lambda k: k['posting_date'] or getdate(nowdate())) + + self.add_payment_entries(non_reconciled_payments) def get_payment_entries(self): order_doctype = "Sales Order" if self.party_type=="Customer" else "Purchase Order" + condition = self.get_conditions(get_payments=True) payment_entries = get_advance_payment_entries(self.party_type, self.party, - self.receivable_payable_account, order_doctype, against_all_orders=True, limit=self.limit) + self.receivable_payable_account, order_doctype, against_all_orders=True, limit=self.payment_limit, + condition=condition) return payment_entries def get_jv_entries(self): + condition = self.get_conditions() dr_or_cr = ("credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency") bank_account_condition = "t2.against_account like %(bank_cash_account)s" \ if self.bank_cash_account else "1=1" - limit_cond = "limit %s" % self.limit if self.limit else "" - journal_entries = frappe.db.sql(""" select "Journal Entry" as reference_type, t1.name as reference_name, @@ -56,7 +64,7 @@ class PaymentReconciliation(Document): where t1.name = t2.parent and t1.docstatus = 1 and t2.docstatus = 1 and t2.party_type = %(party_type)s and t2.party = %(party)s - and t2.account = %(account)s and {dr_or_cr} > 0 + and t2.account = %(account)s and {dr_or_cr} > 0 {condition} and (t2.reference_type is null or t2.reference_type = '' or (t2.reference_type in ('Sales Order', 'Purchase Order') and t2.reference_name is not null and t2.reference_name != '')) @@ -65,11 +73,11 @@ class PaymentReconciliation(Document): THEN 1=1 ELSE {bank_account_condition} END) - order by t1.posting_date {limit_cond} + order by t1.posting_date """.format(**{ "dr_or_cr": dr_or_cr, "bank_account_condition": bank_account_condition, - "limit_cond": limit_cond + "condition": condition }), { "party_type": self.party_type, "party": self.party, @@ -80,6 +88,7 @@ class PaymentReconciliation(Document): return list(journal_entries) def get_dr_or_cr_notes(self): + condition = self.get_conditions(get_return_invoices=True) dr_or_cr = ("credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency") @@ -90,7 +99,7 @@ class PaymentReconciliation(Document): if self.party_type == 'Customer' else "Purchase Invoice") return frappe.db.sql(""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type, - (sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, + (sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date, account_currency as currency FROM `tab{doc}` doc, `tabGL Entry` gl WHERE @@ -100,15 +109,17 @@ class PaymentReconciliation(Document): and gl.against_voucher_type = %(voucher_type)s and doc.docstatus = 1 and gl.party = %(party)s and gl.party_type = %(party_type)s and gl.account = %(account)s - and gl.is_cancelled = 0 + and gl.is_cancelled = 0 {condition} GROUP BY doc.name Having amount > 0 + ORDER BY doc.posting_date """.format( doc=voucher_type, dr_or_cr=dr_or_cr, reconciled_dr_or_cr=reconciled_dr_or_cr, - party_type_field=frappe.scrub(self.party_type)), + party_type_field=frappe.scrub(self.party_type), + condition=condition or ""), { 'party': self.party, 'party_type': self.party_type, @@ -116,22 +127,23 @@ class PaymentReconciliation(Document): 'account': self.receivable_payable_account }, as_dict=1) - def add_payment_entries(self, entries): + def add_payment_entries(self, non_reconciled_payments): self.set('payments', []) - for e in entries: + + for payment in non_reconciled_payments: row = self.append('payments', {}) - row.update(e) + row.update(payment) def get_invoice_entries(self): #Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against - condition = self.check_condition() + condition = self.get_conditions(get_invoices=True) non_reconciled_invoices = get_outstanding_invoices(self.party_type, self.party, self.receivable_payable_account, condition=condition) - if self.limit: - non_reconciled_invoices = non_reconciled_invoices[:self.limit] + if self.invoice_limit: + non_reconciled_invoices = non_reconciled_invoices[:self.invoice_limit] self.add_invoice_entries(non_reconciled_invoices) @@ -139,41 +151,78 @@ class PaymentReconciliation(Document): #Populate 'invoices' with JVs and Invoices to reconcile against self.set('invoices', []) - for e in non_reconciled_invoices: - ent = self.append('invoices', {}) - ent.invoice_type = e.get('voucher_type') - ent.invoice_number = e.get('voucher_no') - ent.invoice_date = e.get('posting_date') - ent.amount = flt(e.get('invoice_amount')) - ent.currency = e.get('currency') - ent.outstanding_amount = e.get('outstanding_amount') + for entry in non_reconciled_invoices: + inv = self.append('invoices', {}) + inv.invoice_type = entry.get('voucher_type') + inv.invoice_number = entry.get('voucher_no') + inv.invoice_date = entry.get('posting_date') + inv.amount = flt(entry.get('invoice_amount')) + inv.currency = entry.get('currency') + inv.outstanding_amount = flt(entry.get('outstanding_amount')) @frappe.whitelist() - def reconcile(self, args): - for e in self.get('payments'): - e.invoice_type = None - if e.invoice_number and " | " in e.invoice_number: - e.invoice_type, e.invoice_number = e.invoice_number.split(" | ") + def allocate_entries(self, args): + self.validate_entries() + entries = [] + for pay in args.get('payments'): + pay.update({'unreconciled_amount': pay.get('amount')}) + for inv in args.get('invoices'): + if pay.get('amount') >= inv.get('outstanding_amount'): + res = self.get_allocated_entry(pay, inv, inv['outstanding_amount']) + pay['amount'] = flt(pay.get('amount')) - flt(inv.get('outstanding_amount')) + inv['outstanding_amount'] = 0 + else: + res = self.get_allocated_entry(pay, inv, pay['amount']) + inv['outstanding_amount'] = flt(inv.get('outstanding_amount')) - flt(pay.get('amount')) + pay['amount'] = 0 + if pay.get('amount') == 0: + entries.append(res) + break + elif inv.get('outstanding_amount') == 0: + entries.append(res) + continue + else: + break - self.get_invoice_entries() - self.validate_invoice() + self.set('allocation', []) + for entry in entries: + if entry['allocated_amount'] != 0: + row = self.append('allocation', {}) + row.update(entry) + + def get_allocated_entry(self, pay, inv, allocated_amount): + return frappe._dict({ + 'reference_type': pay.get('reference_type'), + 'reference_name': pay.get('reference_name'), + 'reference_row': pay.get('reference_row'), + 'invoice_type': inv.get('invoice_type'), + 'invoice_number': inv.get('invoice_number'), + 'unreconciled_amount': pay.get('unreconciled_amount'), + 'amount': pay.get('amount'), + 'allocated_amount': allocated_amount, + 'difference_amount': pay.get('difference_amount') + }) + + @frappe.whitelist() + def reconcile(self): + self.validate_allocation() dr_or_cr = ("credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency") - lst = [] + entry_list = [] dr_or_cr_notes = [] - for e in self.get('payments'): + for row in self.get('allocation'): reconciled_entry = [] - if e.invoice_number and e.allocated_amount: - if e.reference_type in ['Sales Invoice', 'Purchase Invoice']: + if row.invoice_number and row.allocated_amount: + if row.reference_type in ['Sales Invoice', 'Purchase Invoice']: reconciled_entry = dr_or_cr_notes else: - reconciled_entry = lst + reconciled_entry = entry_list - reconciled_entry.append(self.get_payment_details(e, dr_or_cr)) + reconciled_entry.append(self.get_payment_details(row, dr_or_cr)) - if lst: - reconcile_against_document(lst) + if entry_list: + reconcile_against_document(entry_list) if dr_or_cr_notes: reconcile_dr_cr_note(dr_or_cr_notes, self.company) @@ -183,98 +232,104 @@ class PaymentReconciliation(Document): def get_payment_details(self, row, dr_or_cr): return frappe._dict({ - 'voucher_type': row.reference_type, - 'voucher_no' : row.reference_name, - 'voucher_detail_no' : row.reference_row, - 'against_voucher_type' : row.invoice_type, - 'against_voucher' : row.invoice_number, + 'voucher_type': row.get('reference_type'), + 'voucher_no' : row.get('reference_name'), + 'voucher_detail_no' : row.get('reference_row'), + 'against_voucher_type' : row.get('invoice_type'), + 'against_voucher' : row.get('invoice_number'), 'account' : self.receivable_payable_account, 'party_type': self.party_type, 'party': self.party, - 'is_advance' : row.is_advance, + 'is_advance' : row.get('is_advance'), 'dr_or_cr' : dr_or_cr, - 'unadjusted_amount' : flt(row.amount), - 'allocated_amount' : flt(row.allocated_amount), - 'difference_amount': row.difference_amount, - 'difference_account': row.difference_account + 'unreconciled_amount': flt(row.get('unreconciled_amount')), + 'unadjusted_amount' : flt(row.get('amount')), + 'allocated_amount' : flt(row.get('allocated_amount')), + 'difference_amount': flt(row.get('difference_amount')), + 'difference_account': row.get('difference_account') }) - @frappe.whitelist() - def get_difference_amount(self, child_row): - if child_row.get("reference_type") != 'Payment Entry': return - - child_row = frappe._dict(child_row) - - if child_row.invoice_number and " | " in child_row.invoice_number: - child_row.invoice_type, child_row.invoice_number = child_row.invoice_number.split(" | ") - - dr_or_cr = ("credit_in_account_currency" - if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency") - - row = self.get_payment_details(child_row, dr_or_cr) - - doc = frappe.get_doc(row.voucher_type, row.voucher_no) - update_reference_in_payment_entry(row, doc, do_not_save=True) - - return doc.difference_amount - def check_mandatory_to_fetch(self): for fieldname in ["company", "party_type", "party", "receivable_payable_account"]: if not self.get(fieldname): frappe.throw(_("Please select {0} first").format(self.meta.get_label(fieldname))) - def validate_invoice(self): + def validate_entries(self): if not self.get("invoices"): - frappe.throw(_("No records found in the Invoice table")) + frappe.throw(_("No records found in the Invoices table")) if not self.get("payments"): - frappe.throw(_("No records found in the Payment table")) + frappe.throw(_("No records found in the Payments table")) + def validate_allocation(self): unreconciled_invoices = frappe._dict() - for d in self.get("invoices"): - unreconciled_invoices.setdefault(d.invoice_type, {}).setdefault(d.invoice_number, d.outstanding_amount) + + for inv in self.get("invoices"): + unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault(inv.invoice_number, inv.outstanding_amount) invoices_to_reconcile = [] - for p in self.get("payments"): - if p.invoice_type and p.invoice_number and p.allocated_amount: - invoices_to_reconcile.append(p.invoice_number) + for row in self.get("allocation"): + if row.invoice_type and row.invoice_number and row.allocated_amount: + invoices_to_reconcile.append(row.invoice_number) - if p.invoice_number not in unreconciled_invoices.get(p.invoice_type, {}): - frappe.throw(_("{0}: {1} not found in Invoice Details table") - .format(p.invoice_type, p.invoice_number)) + if flt(row.amount) - flt(row.allocated_amount) < 0: + frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}") + .format(row.idx, row.allocated_amount, row.amount)) - if flt(p.allocated_amount) > flt(p.amount): - frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equals to Payment Entry amount {2}") - .format(p.idx, p.allocated_amount, p.amount)) - - invoice_outstanding = unreconciled_invoices.get(p.invoice_type, {}).get(p.invoice_number) - if flt(p.allocated_amount) - invoice_outstanding > 0.009: - frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equals to invoice outstanding amount {2}") - .format(p.idx, p.allocated_amount, invoice_outstanding)) + invoice_outstanding = unreconciled_invoices.get(row.invoice_type, {}).get(row.invoice_number) + if flt(row.allocated_amount) - invoice_outstanding > 0.009: + frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2}") + .format(row.idx, row.allocated_amount, invoice_outstanding)) if not invoices_to_reconcile: - frappe.throw(_("Please select Allocated Amount, Invoice Type and Invoice Number in atleast one row")) + frappe.throw(_("No records found in Allocation table")) - def check_condition(self): - cond = " and posting_date >= {0}".format(frappe.db.escape(self.from_date)) if self.from_date else "" - cond += " and posting_date <= {0}".format(frappe.db.escape(self.to_date)) if self.to_date else "" - dr_or_cr = ("debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' - else "credit_in_account_currency") + def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False): + condition = " and company = '{0}' ".format(self.company) - if self.minimum_amount: - cond += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_amount)) - if self.maximum_amount: - cond += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_amount)) + if get_invoices: + condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date)) if self.from_invoice_date else "" + condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date)) if self.to_invoice_date else "" + dr_or_cr = ("debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' + else "credit_in_account_currency") - return cond + if self.minimum_invoice_amount: + condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount)) + if self.maximum_invoice_amount: + condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_invoice_amount)) + + elif get_return_invoices: + condition = " and doc.company = '{0}' ".format(self.company) + condition += " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else "" + condition += " and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else "" + dr_or_cr = ("gl.debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' + else "gl.credit_in_account_currency") + + if self.minimum_invoice_amount: + condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount)) + if self.maximum_invoice_amount: + condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount)) + + else: + condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else "" + condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else "" + + if self.minimum_payment_amount: + condition += " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) if get_payments \ + else " and total_debit >= {0}".format(flt(self.minimum_payment_amount)) + if self.maximum_payment_amount: + condition += " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) if get_payments \ + else " and total_debit <= {0}".format(flt(self.maximum_payment_amount)) + + return condition def reconcile_dr_cr_note(dr_cr_notes, company): - for d in dr_cr_notes: + for inv in dr_cr_notes: voucher_type = ('Credit Note' - if d.voucher_type == 'Sales Invoice' else 'Debit Note') + if inv.voucher_type == 'Sales Invoice' else 'Debit Note') reconcile_dr_or_cr = ('debit_in_account_currency' - if d.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency') + if inv.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency') company_currency = erpnext.get_company_currency(company) @@ -283,25 +338,25 @@ def reconcile_dr_cr_note(dr_cr_notes, company): "voucher_type": voucher_type, "posting_date": today(), "company": company, - "multi_currency": 1 if d.currency != company_currency else 0, + "multi_currency": 1 if inv.currency != company_currency else 0, "accounts": [ { - 'account': d.account, - 'party': d.party, - 'party_type': d.party_type, - d.dr_or_cr: abs(d.allocated_amount), - 'reference_type': d.against_voucher_type, - 'reference_name': d.against_voucher, + 'account': inv.account, + 'party': inv.party, + 'party_type': inv.party_type, + inv.dr_or_cr: abs(inv.allocated_amount), + 'reference_type': inv.against_voucher_type, + 'reference_name': inv.against_voucher, 'cost_center': erpnext.get_default_cost_center(company) }, { - 'account': d.account, - 'party': d.party, - 'party_type': d.party_type, - reconcile_dr_or_cr: (abs(d.allocated_amount) - if abs(d.unadjusted_amount) > abs(d.allocated_amount) else abs(d.unadjusted_amount)), - 'reference_type': d.voucher_type, - 'reference_name': d.voucher_no, + 'account': inv.account, + 'party': inv.party, + 'party_type': inv.party_type, + reconcile_dr_or_cr: (abs(inv.allocated_amount) + if abs(inv.unadjusted_amount) > abs(inv.allocated_amount) else abs(inv.unadjusted_amount)), + 'reference_type': inv.voucher_type, + 'reference_name': inv.voucher_no, 'cost_center': erpnext.get_default_cost_center(company) } ] diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py new file mode 100644 index 0000000000..87eaaee856 --- /dev/null +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestPaymentReconciliation(unittest.TestCase): + pass diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/__init__.py b/erpnext/accounts/doctype/payment_reconciliation_allocation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json new file mode 100644 index 0000000000..3653501432 --- /dev/null +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -0,0 +1,137 @@ +{ + "actions": [], + "creation": "2021-08-16 17:04:40.185167", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "column_break_3", + "invoice_type", + "invoice_number", + "section_break_6", + "allocated_amount", + "unreconciled_amount", + "amount", + "column_break_8", + "is_advance", + "section_break_5", + "difference_amount", + "column_break_7", + "difference_account" + ], + "fields": [ + { + "fieldname": "invoice_number", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Invoice Number", + "options": "invoice_type", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Allocated Amount", + "options": "Currency", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "difference_account", + "fieldtype": "Link", + "label": "Difference Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "difference_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Difference Amount", + "options": "Currency", + "read_only": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "options": "reference_type", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "is_advance", + "fieldtype": "Data", + "hidden": 1, + "label": "Is Advance", + "read_only": 1 + }, + { + "fieldname": "reference_type", + "fieldtype": "Link", + "label": "Reference Type", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "invoice_type", + "fieldtype": "Link", + "label": "Invoice Type", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "unreconciled_amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Unreconciled Amount", + "options": "Currency", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Amount", + "options": "Currency", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2021-08-30 10:58:42.665107", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Payment Reconciliation Allocation", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py new file mode 100644 index 0000000000..0fb63b1cd1 --- /dev/null +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class PaymentReconciliationAllocation(Document): + pass diff --git a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json index 6a79a85c34..00c9e1240c 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json +++ b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json @@ -44,7 +44,6 @@ { "fieldname": "amount", "fieldtype": "Currency", - "in_list_view": 1, "label": "Amount", "options": "currency", "read_only": 1 @@ -67,7 +66,7 @@ ], "istable": 1, "links": [], - "modified": "2020-07-19 18:12:27.964073", + "modified": "2021-08-24 22:42:40.923179", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Invoice", diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json index 925a6f10a5..add07e870d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json +++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json @@ -11,11 +11,7 @@ "is_advance", "reference_row", "col_break1", - "invoice_number", "amount", - "allocated_amount", - "section_break_10", - "difference_account", "difference_amount", "sec_break1", "remark", @@ -41,6 +37,7 @@ { "fieldname": "posting_date", "fieldtype": "Date", + "in_list_view": 1, "label": "Posting Date", "read_only": 1 }, @@ -62,14 +59,6 @@ "fieldname": "col_break1", "fieldtype": "Column Break" }, - { - "columns": 2, - "fieldname": "invoice_number", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Invoice Number", - "reqd": 1 - }, { "columns": 2, "fieldname": "amount", @@ -79,15 +68,6 @@ "options": "currency", "read_only": 1 }, - { - "columns": 2, - "fieldname": "allocated_amount", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Allocated amount", - "options": "currency", - "reqd": 1 - }, { "fieldname": "sec_break1", "fieldtype": "Section Break" @@ -95,41 +75,27 @@ { "fieldname": "remark", "fieldtype": "Small Text", - "in_list_view": 1, "label": "Remark", "read_only": 1 }, - { - "columns": 2, - "fieldname": "difference_account", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Difference Account", - "options": "Account" - }, - { - "fieldname": "difference_amount", - "fieldtype": "Currency", - "label": "Difference Amount", - "options": "currency", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "section_break_10", - "fieldtype": "Section Break" - }, { "fieldname": "currency", "fieldtype": "Link", "hidden": 1, "label": "Currency", "options": "Currency" + }, + { + "fieldname": "difference_amount", + "fieldtype": "Currency", + "label": "Difference Amount", + "options": "currency", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-07-19 18:12:41.682347", + "modified": "2021-08-30 10:51:48.140062", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Payment", diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.js b/erpnext/accounts/doctype/payment_request/test_payment_request.js deleted file mode 100644 index 070b595fc6..0000000000 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Payment Request", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Payment Request - () => frappe.tests.make('Payment Request', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/payment_term/test_payment_term.js b/erpnext/accounts/doctype/payment_term/test_payment_term.js deleted file mode 100644 index b26e42aa37..0000000000 --- a/erpnext/accounts/doctype/payment_term/test_payment_term.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Payment Term", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Payment Term - () => frappe.tests.make('Payment Term', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.js deleted file mode 100644 index 494a0ed21f..0000000000 --- a/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Payment Terms Template", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Payment Terms Template - () => frappe.tests.make('Payment Terms Template', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index 2d1939131c..2a636bb338 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -13,59 +13,49 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal class TestPeriodClosingVoucher(unittest.TestCase): def test_closing_entry(self): - year_start_date = get_fiscal_year(today(), company="_Test Company")[1] + frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") - make_journal_entry("_Test Bank - _TC", "Sales - _TC", 400, - "_Test Cost Center - _TC", posting_date=now(), submit=True) + company = create_company() + cost_center = create_cost_center('Test Cost Center 1') - make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 600, "_Test Cost Center - _TC", posting_date=now(), submit=True) + jv1 = make_journal_entry( + amount=400, + account1="Cash - TPC", + account2="Sales - TPC", + cost_center=cost_center, + posting_date=now(), + save=False + ) + jv1.company = company + jv1.save() + jv1.submit() - random_expense_account = frappe.db.sql(""" - select t1.account, - sum(t1.debit) - sum(t1.credit) as balance, - sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) \ - as balance_in_account_currency - from `tabGL Entry` t1, `tabAccount` t2 - where t1.account = t2.name and t2.root_type = 'Expense' - and t2.docstatus < 2 and t2.company = '_Test Company' - and t1.posting_date between %s and %s - group by t1.account - having sum(t1.debit) > sum(t1.credit) - limit 1""", (year_start_date, today()), as_dict=True) - - profit_or_loss = frappe.db.sql("""select sum(t1.debit) - sum(t1.credit) as balance - from `tabGL Entry` t1, `tabAccount` t2 - where t1.account = t2.name and t2.report_type = 'Profit and Loss' - and t2.docstatus < 2 and t2.company = '_Test Company' - and t1.posting_date between %s and %s""", (year_start_date, today())) - - profit_or_loss = flt(profit_or_loss[0][0]) if profit_or_loss else 0 + jv2 = make_journal_entry( + amount=600, + account1="Cost of Goods Sold - TPC", + account2="Cash - TPC", + cost_center=cost_center, + posting_date=now(), + save=False + ) + jv2.company = company + jv2.save() + jv2.submit() pcv = self.make_period_closing_voucher() + surplus_account = pcv.closing_account_head - # Check value for closing account - gle_amount_for_closing_account = frappe.db.sql("""select debit - credit - from `tabGL Entry` where voucher_type='Period Closing Voucher' and voucher_no=%s - and account = '_Test Account Reserves and Surplus - _TC'""", pcv.name) + expected_gle = ( + ('Cost of Goods Sold - TPC', 0.0, 600.0), + (surplus_account, 600.0, 400.0), + ('Sales - TPC', 400.0, 0.0) + ) - gle_amount_for_closing_account = flt(gle_amount_for_closing_account[0][0]) \ - if gle_amount_for_closing_account else 0 + pcv_gle = frappe.db.sql(""" + select account, debit, credit from `tabGL Entry` where voucher_no=%s order by account + """, (pcv.name)) - self.assertEqual(gle_amount_for_closing_account, profit_or_loss) - - if random_expense_account: - # Check posted value for teh above random_expense_account - gle_for_random_expense_account = frappe.db.sql(""" - select sum(debit - credit) as amount, - sum(debit_in_account_currency - credit_in_account_currency) as amount_in_account_currency - from `tabGL Entry` - where voucher_type='Period Closing Voucher' and voucher_no=%s and account =%s""", - (pcv.name, random_expense_account[0].account), as_dict=True) - - self.assertEqual(gle_for_random_expense_account[0].amount, -1*random_expense_account[0].balance) - self.assertEqual(gle_for_random_expense_account[0].amount_in_account_currency, - -1*random_expense_account[0].balance_in_account_currency) + self.assertEqual(pcv_gle, expected_gle) def test_cost_center_wise_posting(self): frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") @@ -93,31 +83,23 @@ class TestPeriodClosingVoucher(unittest.TestCase): debit_to="Debtors - TPC" ) - pcv = frappe.get_doc({ - "transaction_date": today(), - "posting_date": today(), - "fiscal_year": get_fiscal_year(today())[0], - "company": "Test PCV Company", - "cost_center_wise_pnl": 1, - "closing_account_head": surplus_account, - "remarks": "Test", - "doctype": "Period Closing Voucher" - }) - pcv.insert() - pcv.submit() + pcv = self.make_period_closing_voucher() + surplus_account = pcv.closing_account_head expected_gle = ( - ('Sales - TPC', 200.0, 0.0, cost_center2), + (surplus_account, 0.0, 400.0, cost_center1), (surplus_account, 0.0, 200.0, cost_center2), ('Sales - TPC', 400.0, 0.0, cost_center1), - (surplus_account, 0.0, 400.0, cost_center1) + ('Sales - TPC', 200.0, 0.0, cost_center2), ) pcv_gle = frappe.db.sql(""" - select account, debit, credit, cost_center from `tabGL Entry` where voucher_no=%s + select account, debit, credit, cost_center + from `tabGL Entry` where voucher_no=%s + order by account, cost_center """, (pcv.name)) - self.assertTrue(pcv_gle, expected_gle) + self.assertEqual(pcv_gle, expected_gle) def test_period_closing_with_finance_book_entries(self): frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") @@ -146,39 +128,35 @@ class TestPeriodClosingVoucher(unittest.TestCase): jv.save() jv.submit() - pcv = frappe.get_doc({ - "transaction_date": today(), - "posting_date": today(), - "fiscal_year": get_fiscal_year(today())[0], - "company": company, - "closing_account_head": surplus_account, - "remarks": "Test", - "doctype": "Period Closing Voucher" - }) - pcv.insert() - pcv.submit() + pcv = self.make_period_closing_voucher() + surplus_account = pcv.closing_account_head expected_gle = ( - (surplus_account, 0.0, 400.0, ''), + (surplus_account, 0.0, 400.0, None), (surplus_account, 0.0, 400.0, jv.finance_book), - ('Sales - TPC', 400.0, 0.0, ''), + ('Sales - TPC', 400.0, 0.0, None), ('Sales - TPC', 400.0, 0.0, jv.finance_book) ) pcv_gle = frappe.db.sql(""" - select account, debit, credit, finance_book from `tabGL Entry` where voucher_no=%s + select account, debit, credit, finance_book + from `tabGL Entry` where voucher_no=%s + order by account, finance_book """, (pcv.name)) - self.assertTrue(pcv_gle, expected_gle) + self.assertEqual(pcv_gle, expected_gle) def make_period_closing_voucher(self): + surplus_account = create_account() + cost_center = create_cost_center("Test Cost Center 1") pcv = frappe.get_doc({ "doctype": "Period Closing Voucher", - "closing_account_head": "_Test Account Reserves and Surplus - _TC", - "company": "_Test Company", - "fiscal_year": get_fiscal_year(today(), company="_Test Company")[0], + "transaction_date": today(), "posting_date": today(), - "cost_center": "_Test Cost Center - _TC", + "company": "Test PCV Company", + "fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0], + "cost_center": cost_center, + "closing_account_head": surplus_account, "remarks": "test" }) pcv.insert() diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.js deleted file mode 100644 index 48109b159c..0000000000 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: POS Closing Entry", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new POS Closing Entry - () => frappe.tests.make('POS Closing Entry', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.js b/erpnext/accounts/doctype/pos_profile/test_pos_profile.js deleted file mode 100644 index 42e5b7f92f..0000000000 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: POS Profile", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('POS Profile', [ - // insert a new POS Profile - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/pos_profile_user/test_pos_profile_user.js b/erpnext/accounts/doctype/pos_profile_user/test_pos_profile_user.js deleted file mode 100644 index 5449ab76a3..0000000000 --- a/erpnext/accounts/doctype/pos_profile_user/test_pos_profile_user.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: POS Profile User", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new POS Profile User - () => frappe.tests.make('POS Profile User', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/pos_settings/test_pos_settings.js b/erpnext/accounts/doctype/pos_settings/test_pos_settings.js deleted file mode 100644 index 639c94ed10..0000000000 --- a/erpnext/accounts/doctype/pos_settings/test_pos_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: POS Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new POS Settings - () => frappe.tests.make('POS Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 94abf3b3c0..5467cb0bc5 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -475,7 +475,20 @@ def apply_pricing_rule_on_transaction(doc): frappe.msgprint(_("User has not applied rule on the invoice {0}") .format(doc.name)) else: - doc.set(field, d.get(pr_field)) + if not d.coupon_code_based: + doc.set(field, d.get(pr_field)) + elif doc.get('coupon_code'): + # coupon code based pricing rule + coupon_code_pricing_rule = frappe.db.get_value('Coupon Code', doc.get('coupon_code'), 'pricing_rule') + if coupon_code_pricing_rule == d.name: + # if selected coupon code is linked with pricing rule + doc.set(field, d.get(pr_field)) + else: + # reset discount if not linked + doc.set(field, 0) + else: + # if coupon code based but no coupon code selected + doc.set(field, 0) doc.calculate_taxes_and_totals() elif d.price_or_product_discount == 'Product': diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index a16795e628..e2f02f37ee 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -415,6 +415,8 @@ class PurchaseInvoice(BuyingController): self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) + self.process_common_party_accounting() + def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: gl_entries = self.get_gl_entries() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 1cf0df00db..fe3ed1670d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -253,6 +253,8 @@ class SalesInvoice(SellingController): if "Healthcare" in active_domains: manage_invoice_submit_cancel(self, "on_submit") + self.process_common_party_accounting() + def validate_pos_return(self): if self.is_pos and self.is_return: diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index c3d83c7d74..e06a3bb5b1 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -151,7 +151,7 @@ class TestSalesInvoice(unittest.TestCase): si1 = create_sales_invoice(rate=1000) si2 = create_sales_invoice(rate=300) si3 = create_sales_invoice(qty=-1, rate=300, is_return=1) - + pe = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Bank - _TC") pe.append('references', { @@ -1140,6 +1140,18 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(loss_for_si['credit'], loss_for_return_si['debit']) self.assertEqual(loss_for_si['debit'], loss_for_return_si['credit']) + def test_incoming_rate_for_stand_alone_credit_note(self): + return_si = create_sales_invoice(is_return=1, update_stock=1, qty=-1, rate=90000, incoming_rate=10, + company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', debit_to='Debtors - TCP1', + income_account='Sales - TCP1', expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1') + + incoming_rate = frappe.db.get_value('Stock Ledger Entry', {'voucher_no': return_si.name}, 'incoming_rate') + debit_amount = frappe.db.get_value('GL Entry', + {'voucher_no': return_si.name, 'account': 'Stock In Hand - TCP1'}, 'debit') + + self.assertEqual(debit_amount, 10.0) + self.assertEqual(incoming_rate, 10.0) + def test_discount_on_net_total(self): si = frappe.copy_doc(test_records[2]) si.apply_discount_on = "Net Total" @@ -1816,23 +1828,13 @@ class TestSalesInvoice(unittest.TestCase): acc_settings.save() def test_inter_company_transaction(self): + from erpnext.selling.doctype.customer.test_customer import create_internal_customer - if not frappe.db.exists("Customer", "_Test Internal Customer"): - customer = frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "_Test Internal Customer", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory", - "is_internal_customer": 1, - "represents_company": "_Test Company 1" - }) - - customer.append("companies", { - "company": "Wind Power LLC" - }) - - customer.insert() + create_internal_customer( + customer_name="_Test Internal Customer", + represents_company="_Test Company 1", + allowed_to_interact_with="Wind Power LLC" + ) if not frappe.db.exists("Supplier", "_Test Internal Supplier"): supplier = frappe.get_doc({ @@ -1958,8 +1960,43 @@ class TestSalesInvoice(unittest.TestCase): frappe.local.enable_perpetual_inventory['_Test Company 1'] = old_perpetual_inventory frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock) + def test_sle_if_target_warehouse_exists_accidentally(self): + """ + Check if inward entry exists if Target Warehouse accidentally exists + but Customer is not an internal customer. + """ + se = make_stock_entry( + item_code="138-CMS Shoe", + target="Finished Goods - _TC", + company = "_Test Company", + qty=1, + basic_rate=500 + ) + + si = frappe.copy_doc(test_records[0]) + si.update_stock = 1 + si.set_warehouse = "Finished Goods - _TC" + si.set_target_warehouse = "Stores - _TC" + si.get("items")[0].warehouse = "Finished Goods - _TC" + si.get("items")[0].target_warehouse = "Stores - _TC" + si.insert() + si.submit() + + sles = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": si.name}, + fields=["name", "actual_qty"]) + + # check if only one SLE for outward entry is created + self.assertEqual(len(sles), 1) + self.assertEqual(sles[0].actual_qty, -1) + + # tear down + si.cancel() + se.cancel() + def test_internal_transfer_gl_entry(self): ## Create internal transfer account + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + account = create_account(account_name="Unrealized Profit", parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") @@ -2163,6 +2200,50 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) self.assertTrue(schedule.journal_entry) + def test_sales_invoice_against_supplier(self): + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import make_customer + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + # create a customer + customer = make_customer(customer="_Test Common Supplier") + # create a supplier + supplier = create_supplier(supplier_name="_Test Common Supplier").name + + # create a party link between customer & supplier + # set primary role as supplier + party_link = frappe.new_doc("Party Link") + party_link.primary_role = "Supplier" + party_link.primary_party = supplier + party_link.secondary_role = "Customer" + party_link.secondary_party = customer + party_link.save() + + # enable common party accounting + frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1) + + # create a sales invoice + si = create_sales_invoice(customer=customer, parent_cost_center="_Test Cost Center - _TC") + + # check outstanding of sales invoice + si.reload() + self.assertEqual(si.status, 'Paid') + self.assertEqual(flt(si.outstanding_amount), 0.0) + + # check creation of journal entry + jv = frappe.get_all('Journal Entry Account', { + 'account': si.debit_to, + 'party_type': 'Customer', + 'party': si.customer, + 'reference_type': si.doctype, + 'reference_name': si.name + }, pluck='credit_in_account_currency') + + self.assertTrue(jv) + self.assertEqual(jv[0], si.grand_total) + + party_link.delete() + frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' @@ -2375,7 +2456,8 @@ def create_sales_invoice(**args): "asset": args.asset or None, "cost_center": args.cost_center or "_Test Cost Center - _TC", "serial_no": args.serial_no, - "conversion_factor": 1 + "conversion_factor": 1, + "incoming_rate": args.incoming_rate or 0 }) if not args.do_not_save: @@ -2472,29 +2554,6 @@ def get_taxes_and_charges(): "row_id": 1 }] -def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): - if not frappe.db.exists("Customer", customer_name): - customer = frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": customer_name, - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory", - "is_internal_customer": 1, - "represents_company": represents_company - }) - - customer.append("companies", { - "company": allowed_to_interact_with - }) - - customer.insert() - customer_name = customer.name - else: - customer_name = frappe.db.get_value("Customer", customer_name) - - return customer_name - def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with): if not frappe.db.exists("Supplier", supplier_name): supplier = frappe.get_doc({ diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index c77076cb90..b90f3f0904 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -53,7 +53,6 @@ "column_break_24", "base_net_rate", "base_net_amount", - "incoming_rate", "drop_ship", "delivered_by_supplier", "accounting", @@ -81,6 +80,7 @@ "target_warehouse", "quality_inspection", "batch_no", + "incoming_rate", "col_break5", "allow_zero_valuation_rate", "serial_no", @@ -807,12 +807,12 @@ "read_only": 1 }, { + "depends_on": "eval:parent.is_return && parent.update_stock && !parent.return_against", "fieldname": "incoming_rate", "fieldtype": "Currency", - "label": "Incoming Rate", + "label": "Incoming Rate (Costing)", "no_copy": 1, - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "depends_on": "eval: doc.uom != doc.stock_uom", @@ -833,7 +833,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-12 20:15:47.668399", + "modified": "2021-08-19 13:41:53.435827", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js index 0e011883b1..97a6fdd336 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js @@ -5,25 +5,3 @@ cur_frm.cscript.tax_table = "Sales Taxes and Charges"; {% include "erpnext/public/js/controllers/accounts.js" %} -frappe.tour['Sales Taxes and Charges Template'] = [ - { - fieldname: "title", - title: __("Title"), - description: __("A name by which you will identify this template. You can change this later."), - }, - { - fieldname: "company", - title: __("Company"), - description: __("Company for which this tax template will be applicable"), - }, - { - fieldname: "is_default", - title: __("Is this Default?"), - description: __("Set this template as the default for all sales transactions"), - }, - { - fieldname: "taxes", - title: __("Taxes Table"), - description: __("You can add a row for a tax rule here. These rules can be applied on the net total, or can be a flat amount."), - } -]; diff --git a/erpnext/accounts/doctype/share_transfer/test_share_transfer.js b/erpnext/accounts/doctype/share_transfer/test_share_transfer.js deleted file mode 100644 index e5530fa0aa..0000000000 --- a/erpnext/accounts/doctype/share_transfer/test_share_transfer.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Share Transfer", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Share Transfer - () => frappe.tests.make('Share Transfer', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/share_type/test_share_type.js b/erpnext/accounts/doctype/share_type/test_share_type.js deleted file mode 100644 index 620afa2ba8..0000000000 --- a/erpnext/accounts/doctype/share_type/test_share_type.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Share Type", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Share Type - () => frappe.tests.make('Share Type', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/shareholder/test_shareholder.js b/erpnext/accounts/doctype/shareholder/test_shareholder.js deleted file mode 100644 index 61c53120ea..0000000000 --- a/erpnext/accounts/doctype/shareholder/test_shareholder.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Shareholder", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Shareholder - () => frappe.tests.make('Shareholder', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.js b/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.js deleted file mode 100644 index 15d3df2a63..0000000000 --- a/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Subscription Invoice", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Subscription Invoice - () => frappe.tests.make('Subscription Invoice', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json index 771611a786..878ae09889 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json @@ -21,7 +21,7 @@ "column_break_13", "billing_interval_count", "payment_plan_section", - "payment_plan_id", + "product_price_id", "column_break_16", "payment_gateway", "accounting_dimensions_section", @@ -114,11 +114,6 @@ "fieldtype": "Section Break", "label": "Payment Plan" }, - { - "fieldname": "payment_plan_id", - "fieldtype": "Data", - "label": "Payment Plan" - }, { "fieldname": "column_break_16", "fieldtype": "Column Break" @@ -144,10 +139,15 @@ "fieldtype": "Link", "label": "Cost Center", "options": "Cost Center" + }, + { + "fieldname": "product_price_id", + "fieldtype": "Data", + "label": "Product Price ID" } ], "links": [], - "modified": "2021-08-09 10:53:44.205774", + "modified": "2021-08-13 10:53:44.205774", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Plan", diff --git a/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.js b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.js deleted file mode 100644 index 3ceb9a6050..0000000000 --- a/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Subscription Plan", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Subscription Plan - () => frappe.tests.make('Subscription Plan', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.js b/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.js deleted file mode 100644 index 5a751ea99c..0000000000 --- a/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Subscription Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Subscription Settings - () => frappe.tests.make('Subscription Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/tax_category/test_tax_category.js b/erpnext/accounts/doctype/tax_category/test_tax_category.js deleted file mode 100644 index 5142456d76..0000000000 --- a/erpnext/accounts/doctype/tax_category/test_tax_category.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Tax Category", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Tax Category - () => frappe.tests.make('Tax Category', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.js b/erpnext/accounts/doctype/tax_rule/test_tax_rule.js deleted file mode 100644 index 72d177deff..0000000000 --- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Tax Rule", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Tax Rule - () => frappe.tests.make('Tax Rule', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.js b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.js deleted file mode 100644 index eab98d4389..0000000000 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Tax Withholding Category", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Tax Withholding Category - () => frappe.tests.make('Tax Withholding Category', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/form_tour/accounts_settings/accounts_settings.json b/erpnext/accounts/form_tour/accounts_settings/accounts_settings.json new file mode 100644 index 0000000000..e2bf50d20a --- /dev/null +++ b/erpnext/accounts/form_tour/accounts_settings/accounts_settings.json @@ -0,0 +1,113 @@ +{ + "creation": "2021-06-29 17:00:18.273054", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-06-29 17:00:26.145996", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Accounts Settings", + "owner": "Administrator", + "reference_doctype": "Accounts Settings", + "save_on_complete": 0, + "steps": [ + { + "description": "The percentage by which you can overbill transactions. For example, if the order value is $100 for an Item and percentage here is set as 10% then you are allowed to bill for $110.", + "field": "", + "fieldname": "over_billing_allowance", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Over Billing Allowance (%)", + "parent_field": "", + "position": "Right", + "title": "Over Billing Allowance Percentage" + }, + { + "description": "Select the role that is allowed to overbill a transactions.", + "field": "", + "fieldname": "role_allowed_to_over_bill", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Role Allowed to Over Bill ", + "parent_field": "", + "position": "Right", + "title": "Role Allowed to Over Bill" + }, + { + "description": "If checked, system will unlink the payment against the respective invoice.", + "field": "", + "fieldname": "unlink_payment_on_cancellation_of_invoice", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Unlink Payment on Cancellation of Invoice", + "parent_field": "", + "position": "Bottom", + "title": "Unlink Payment on Cancellation of Invoice" + }, + { + "description": "Similar to the previous option, this unlinks any advance payments made against Purchase/Sales Orders.", + "field": "", + "fieldname": "unlink_advance_payment_on_cancelation_of_order", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Unlink Advance Payment on Cancellation of Order", + "parent_field": "", + "position": "Bottom", + "title": "Unlink Advance Payment on Cancellation of Order" + }, + { + "description": "Tax category can be set on Addresses. An address can be Shipping or Billing address. Set which addres to select when applying Tax Category.", + "field": "", + "fieldname": "determine_address_tax_category_from", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Determine Address Tax Category From", + "parent_field": "", + "position": "Right", + "title": "Determine Address Tax Category From" + }, + { + "description": "Freeze accounting transactions up to specified date, nobody can make/modify entry except the specified Role.", + "field": "", + "fieldname": "acc_frozen_upto", + "fieldtype": "Date", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Accounts Frozen Till Date", + "parent_field": "", + "position": "Right", + "title": "Accounts Frozen Upto" + }, + { + "description": "Users with this Role are allowed to set frozen accounts and create/modify accounting entries against frozen accounts.", + "field": "", + "fieldname": "frozen_accounts_modifier", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries", + "parent_field": "", + "position": "Right", + "title": "Role Allowed to Set Frozen Accounts & Edit Frozen Entries" + }, + { + "description": "Select the role that is allowed to submit transactions that exceed credit limits set. The credit limit can be set in the Customer form.", + "field": "", + "fieldname": "credit_controller", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Credit Controller", + "parent_field": "", + "position": "Left", + "title": "Credit Controller" + } + ], + "title": "Accounts Settings" +} \ No newline at end of file diff --git a/erpnext/accounts/form_tour/purchase_invoice/purchase_invoice.json b/erpnext/accounts/form_tour/purchase_invoice/purchase_invoice.json new file mode 100644 index 0000000000..2dffcd1c0e --- /dev/null +++ b/erpnext/accounts/form_tour/purchase_invoice/purchase_invoice.json @@ -0,0 +1,96 @@ +{ + "creation": "2021-06-29 16:31:48.558826", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-06-29 16:31:48.558826", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Purchase Invoice", + "owner": "Administrator", + "reference_doctype": "Purchase Invoice", + "save_on_complete": 1, + "steps": [ + { + "description": "Select Supplier", + "field": "", + "fieldname": "supplier", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Supplier", + "next_step_condition": "supplier", + "parent_field": "", + "position": "Right", + "title": "Select Supplier" + }, + { + "description": "Add items in the table", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Items", + "parent_field": "", + "position": "Bottom", + "title": "List of Items" + }, + { + "child_doctype": "Purchase Invoice Item", + "description": "Select an item", + "field": "", + "fieldname": "item_code", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 1, + "label": "Item", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Select Item" + }, + { + "child_doctype": "Purchase Invoice Item", + "description": "Enter the quantity", + "field": "", + "fieldname": "qty", + "fieldtype": "Float", + "has_next_condition": 0, + "is_table_field": 1, + "label": "Accepted Qty", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Enter Quantity" + }, + { + "child_doctype": "Purchase Invoice Item", + "description": "Enter rate of the item", + "field": "", + "fieldname": "rate", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 1, + "label": "Rate", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Enter Rate" + }, + { + "description": "You can add taxes here", + "field": "", + "fieldname": "taxes", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Purchase Taxes and Charges", + "parent_field": "", + "position": "Bottom", + "title": "Select taxes" + } + ], + "title": "Purchase Invoice" +} \ No newline at end of file diff --git a/erpnext/accounts/form_tour/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json b/erpnext/accounts/form_tour/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json new file mode 100644 index 0000000000..7de9ae1539 --- /dev/null +++ b/erpnext/accounts/form_tour/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json @@ -0,0 +1,65 @@ +{ + "creation": "2021-08-24 12:28:18.044902", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-24 12:28:18.044902", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Taxes and Charges Template", + "owner": "Administrator", + "reference_doctype": "Sales Taxes and Charges Template", + "save_on_complete": 0, + "steps": [ + { + "description": "A name by which you will identify this template. You can change this later.", + "field": "", + "fieldname": "title", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Title", + "parent_field": "", + "position": "Bottom", + "title": "Title" + }, + { + "description": "Company for which this tax template will be applicable", + "field": "", + "fieldname": "company", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Company", + "parent_field": "", + "position": "Bottom", + "title": "Company" + }, + { + "description": "Set this template as the default for all sales transactions", + "field": "", + "fieldname": "is_default", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default", + "parent_field": "", + "position": "Bottom", + "title": "Is this Default Tax Template?" + }, + { + "description": "You can add a row for a tax rule here. These rules can be applied on the net total, or can be a flat amount.", + "field": "", + "fieldname": "taxes", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Sales Taxes and Charges", + "parent_field": "", + "position": "Bottom", + "title": "Taxes Table" + } + ], + "title": "Sales Taxes and Charges Template" +} \ No newline at end of file diff --git a/erpnext/accounts/module_onboarding/accounts/accounts.json b/erpnext/accounts/module_onboarding/accounts/accounts.json index 6b5c5a1db8..2e0ab4305d 100644 --- a/erpnext/accounts/module_onboarding/accounts/accounts.json +++ b/erpnext/accounts/module_onboarding/accounts/accounts.json @@ -13,12 +13,15 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts", "idx": 0, "is_complete": 0, - "modified": "2020-10-30 15:41:15.547225", + "modified": "2021-08-13 11:59:35.690443", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts", "owner": "Administrator", "steps": [ + { + "step": "Company" + }, { "step": "Chart of Accounts" }, @@ -26,22 +29,19 @@ "step": "Setup Taxes" }, { - "step": "Create a Product" + "step": "Accounts Settings" }, { - "step": "Create a Supplier" + "step": "Cost Centers for Report and Budgeting" }, { "step": "Create Your First Purchase Invoice" }, { - "step": "Create a Customer" + "step": "Updating Opening Balances" }, { - "step": "Create Your First Sales Invoice" - }, - { - "step": "Configure Account Settings" + "step": "Financial Statements" } ], "subtitle": "Accounts, Invoices, Taxation, and more.", diff --git a/erpnext/accounts/onboarding_step/accounts_settings/accounts_settings.json b/erpnext/accounts/onboarding_step/accounts_settings/accounts_settings.json new file mode 100644 index 0000000000..3f44a73685 --- /dev/null +++ b/erpnext/accounts/onboarding_step/accounts_settings/accounts_settings.json @@ -0,0 +1,21 @@ +{ + "action": "Show Form Tour", + "action_label": "Take a quick walk-through of Accounts Settings", + "creation": "2021-06-29 16:42:03.400731", + "description": "# Account Settings\n\nIn ERPNext, Accounting features are configurable as per your business needs. Accounts Settings is the place to define some of your accounting preferences like:\n\n - Credit Limit and over billing settings\n - Taxation preferences\n - Deferred accounting preferences\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 1, + "is_skipped": 0, + "modified": "2021-08-13 11:50:06.227835", + "modified_by": "Administrator", + "name": "Accounts Settings", + "owner": "Administrator", + "reference_document": "Accounts Settings", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Accounts Settings", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json index fc49bd652b..67553baec7 100644 --- a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json +++ b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json @@ -1,10 +1,10 @@ { - "action": "Go to Page", - "action_label": "View Chart of Accounts", + "action": "Watch Video", + "action_label": "Learn more about Chart of Accounts", "callback_message": "You can continue with the onboarding after exploring this page", "callback_title": "Awesome Work", "creation": "2020-05-13 19:58:20.928127", - "description": "# Chart Of Accounts\n\nThe Chart of Accounts is the blueprint of the accounts in your organization.\nIt is a tree view of the names of the Accounts (Ledgers and Groups) that a Company requires to manage its books of accounts. ERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to your needs and legal requirements.\n\nFor each company, Chart of Accounts signifies the way to classify the accounting entries, mostly\nbased on statutory (tax, compliance to government regulations) requirements.\n\nThere's a brief video tutorial about chart of accounts in the next step.", + "description": "# Chart Of Accounts\n\nERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to business and legal requirements.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, @@ -12,7 +12,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-30 14:35:59.474920", + "modified": "2021-08-13 11:46:25.878506", "modified_by": "Administrator", "name": "Chart of Accounts", "owner": "Administrator", @@ -21,5 +21,6 @@ "show_form_tour": 0, "show_full_form": 0, "title": "Review Chart of Accounts", - "validate_action": 0 + "validate_action": 0, + "video_url": "https://www.youtube.com/embed/AcfMCT7wLLo" } \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/company/company.json b/erpnext/accounts/onboarding_step/company/company.json new file mode 100644 index 0000000000..4992e4d018 --- /dev/null +++ b/erpnext/accounts/onboarding_step/company/company.json @@ -0,0 +1,22 @@ +{ + "action": "Go to Page", + "action_label": "Let's Review your Company", + "creation": "2021-06-29 14:47:42.497318", + "description": "# Company\n\nIn ERPNext, you can also create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company. \n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-13 11:43:35.767341", + "modified_by": "Administrator", + "name": "Company", + "owner": "Administrator", + "path": "app/company", + "reference_document": "Company", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Review Company", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/cost_centers_for_report_and_budgeting/cost_centers_for_report_and_budgeting.json b/erpnext/accounts/onboarding_step/cost_centers_for_report_and_budgeting/cost_centers_for_report_and_budgeting.json new file mode 100644 index 0000000000..252b075697 --- /dev/null +++ b/erpnext/accounts/onboarding_step/cost_centers_for_report_and_budgeting/cost_centers_for_report_and_budgeting.json @@ -0,0 +1,21 @@ +{ + "action": "Go to Page", + "action_label": "View Cost Center Tree", + "creation": "2021-07-12 12:02:05.726608", + "description": "# Cost Centers for Budgeting and Analysis\n\nWhile your Books of Accounts are framed to fulfill statutory requirements, you can set up Cost Center and Accounting Dimensions to address your companies reporting and budgeting requirements.\n\nClick here to learn more about how [Cost Center](https://docs.erpnext.com/docs/v13/user/manual/en/accounts/cost-center) and [Dimensions](https://docs.erpnext.com/docs/v13/user/manual/en/accounts/accounting-dimensions) allow you to get advanced financial analytics reports from ERPNext.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-13 11:55:08.510366", + "modified_by": "Administrator", + "name": "Cost Centers for Report and Budgeting", + "owner": "Administrator", + "path": "cost-center/view/tree", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Cost Centers for Budgeting and Analysis", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json b/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json index ddbc89ec0a..f4e298e701 100644 --- a/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json +++ b/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json @@ -1,14 +1,15 @@ { - "action": "Create Entry", + "action": "Show Form Tour", + "action_label": "Let\u2019s create your first Purchase Invoice", "creation": "2020-05-14 22:10:07.049704", - "description": "# What's a Purchase Invoice?\n\nA Purchase Invoice is a bill you receive from your Suppliers against which you need to make the payment.\nPurchase Invoice is the exact opposite of your Sales Invoice. Here you accrue expenses to your Supplier. \n\nThe following is what a typical purchase cycle looks like, however you can create a purchase invoice directly as well.\n\n![Purchase Flow](https://docs.erpnext.com/docs/assets/img/accounts/pi-flow.png)\n\n", + "description": "# Create your first Purchase Invoice\n\nA Purchase Invoice is a bill received from a Supplier for a product(s) or service(s) delivery to your company. You can track payables through Purchase Invoice and process Payment Entries against it.\n\nPurchase Invoices can also be created against a Purchase Order or Purchase Receipt.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-30 15:30:26.337773", + "modified": "2021-08-13 11:56:11.677253", "modified_by": "Administrator", "name": "Create Your First Purchase Invoice", "owner": "Administrator", diff --git a/erpnext/accounts/onboarding_step/financial_statements/financial_statements.json b/erpnext/accounts/onboarding_step/financial_statements/financial_statements.json new file mode 100644 index 0000000000..85cf9cdd7c --- /dev/null +++ b/erpnext/accounts/onboarding_step/financial_statements/financial_statements.json @@ -0,0 +1,23 @@ +{ + "action": "View Report", + "creation": "2021-07-12 12:08:47.026115", + "description": "# Financial Statements\n\nIn ERPNext, you can get crucial financial reports like [Balance Sheet] and [Profit and Loss] statements with a click of a button. You can run in the report for a different period and plot analytics charts premised on statement data. For more reports, check sections like Financial Statements, General Ledger, and Profitability reports.\n\n[Check Accounting reports](https://docs.erpnext.com/docs/v13/user/manual/en/accounts/accounting-reports)", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-13 11:59:18.767407", + "modified_by": "Administrator", + "name": "Financial Statements", + "owner": "Administrator", + "reference_report": "General Ledger", + "report_description": "General Ledger", + "report_reference_doctype": "GL Entry", + "report_type": "Script Report", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Financial Statements", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json index a4922013da..9f4c873e34 100644 --- a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json +++ b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json @@ -1,21 +1,21 @@ { "action": "Create Entry", - "action_label": "Make a Sales Tax Template", + "action_label": "Manage Sales Tax Templates", "creation": "2020-05-13 19:29:43.844463", - "description": "# Setting up Taxes\n\nAny sophisticated accounting system, including ERPNext will have automatic tax calculations for your transactions. These calculations are based on user defined rules in compliance to local rules and regulations.\n\nERPNext allows this via *Tax Templates*. These templates can be used in Sales Orders and Sales Invoices. Other types of charges that may apply to your invoices (like shipping, insurance etc.) can also be configured as taxes.\n\nFor Tax Accounts that you want to use in the tax templates, go to:\n\n`> Accounting > Taxes > Sales Taxes and Charges Template`\n\nYou can read more about these templates in our documentation [here](https://docs.erpnext.com/docs/user/manual/en/selling/sales-taxes-and-charges-template)\n", + "description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-30 14:54:18.087383", + "modified": "2021-08-13 11:48:37.238610", "modified_by": "Administrator", "name": "Setup Taxes", "owner": "Administrator", "reference_document": "Sales Taxes and Charges Template", "show_form_tour": 1, "show_full_form": 1, - "title": "Lets create a Tax Template for Sales ", + "title": "Setting up Taxes", "validate_action": 0 } \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/updating_opening_balances/updating_opening_balances.json b/erpnext/accounts/onboarding_step/updating_opening_balances/updating_opening_balances.json new file mode 100644 index 0000000000..c0849a4ef5 --- /dev/null +++ b/erpnext/accounts/onboarding_step/updating_opening_balances/updating_opening_balances.json @@ -0,0 +1,22 @@ +{ + "action": "Watch Video", + "action_label": "Learn how to update opening balances", + "creation": "2021-07-12 11:53:50.525030", + "description": "# Updating Opening Balances\n\nOnce you close the financial statement in previous accounting software, you can update the same as opening in your ERPNext's Balance Sheet accounts. This will allow you to get complete financial statements from ERPNext in the coming years, and discontinue the parallel accounting system right away.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "intro_video_url": "https://www.youtube.com/embed/U5wPIvEn-0c", + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-13 11:56:45.483418", + "modified_by": "Administrator", + "name": "Updating Opening Balances", + "owner": "Administrator", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Updating Opening Balances", + "validate_action": 1, + "video_url": "https://www.youtube.com/embed/U5wPIvEn-0c" +} \ No newline at end of file diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 118f628abe..c46eb7e631 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -341,31 +341,42 @@ def add_cc(args=None): def reconcile_against_document(args): """ - Cancel JV, Update aginst document, split if required and resubmit jv + Cancel PE or JV, Update against document, split if required and resubmit """ - for d in args: + # To optimize making GL Entry for PE or JV with multiple references + reconciled_entries = {} + for row in args: + if not reconciled_entries.get((row.voucher_type, row.voucher_no)): + reconciled_entries[(row.voucher_type, row.voucher_no)] = [] - check_if_advance_entry_modified(d) - validate_allocated_amount(d) + reconciled_entries[(row.voucher_type, row.voucher_no)].append(row) + + for key, entries in reconciled_entries.items(): + voucher_type = key[0] + voucher_no = key[1] # cancel advance entry - doc = frappe.get_doc(d.voucher_type, d.voucher_no) - + doc = frappe.get_doc(voucher_type, voucher_no) frappe.flags.ignore_party_validation = True doc.make_gl_entries(cancel=1, adv_adj=1) - # update ref in advance entry - if d.voucher_type == "Journal Entry": - update_reference_in_journal_entry(d, doc) - else: - update_reference_in_payment_entry(d, doc) + for entry in entries: + check_if_advance_entry_modified(entry) + validate_allocated_amount(entry) + # update ref in advance entry + if voucher_type == "Journal Entry": + update_reference_in_journal_entry(entry, doc, do_not_save=True) + else: + update_reference_in_payment_entry(entry, doc, do_not_save=True) + + doc.save(ignore_permissions=True) # re-submit advance entry - doc = frappe.get_doc(d.voucher_type, d.voucher_no) + doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) doc.make_gl_entries(cancel = 0, adv_adj =1) frappe.flags.ignore_party_validation = False - if d.voucher_type in ('Payment Entry', 'Journal Entry'): + if entry.voucher_type in ('Payment Entry', 'Journal Entry'): doc.update_expense_claim() def check_if_advance_entry_modified(args): @@ -374,6 +385,9 @@ def check_if_advance_entry_modified(args): check if amount is same check if jv is submitted """ + if not args.get('unreconciled_amount'): + args.update({'unreconciled_amount': args.get('unadjusted_amount')}) + ret = None if args.voucher_type == "Journal Entry": ret = frappe.db.sql(""" @@ -395,14 +409,14 @@ def check_if_advance_entry_modified(args): and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s and t1.party_type = %(party_type)s and t1.party = %(party)s and t1.{0} = %(account)s and t2.reference_doctype in ("", "Sales Order", "Purchase Order") - and t2.allocated_amount = %(unadjusted_amount)s + and t2.allocated_amount = %(unreconciled_amount)s """.format(party_account_field), args) else: ret = frappe.db.sql("""select name from `tabPayment Entry` where name = %(voucher_no)s and docstatus = 1 and party_type = %(party_type)s and party = %(party)s and {0} = %(account)s - and unallocated_amount = %(unadjusted_amount)s + and unallocated_amount = %(unreconciled_amount)s """.format(party_account_field), args) if not ret: @@ -415,58 +429,44 @@ def validate_allocated_amount(args): elif flt(args.get("allocated_amount"), precision) > flt(args.get("unadjusted_amount"), precision): throw(_("Allocated amount cannot be greater than unadjusted amount")) -def update_reference_in_journal_entry(d, jv_obj): +def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): """ Updates against document, if partial amount splits into rows """ - jv_detail = jv_obj.get("accounts", {"name": d["voucher_detail_no"]})[0] - jv_detail.set(d["dr_or_cr"], d["allocated_amount"]) - jv_detail.set('debit' if d['dr_or_cr']=='debit_in_account_currency' else 'credit', - d["allocated_amount"]*flt(jv_detail.exchange_rate)) - - original_reference_type = jv_detail.reference_type - original_reference_name = jv_detail.reference_name - - jv_detail.set("reference_type", d["against_voucher_type"]) - jv_detail.set("reference_name", d["against_voucher"]) - - if d['allocated_amount'] < d['unadjusted_amount']: - jvd = frappe.db.sql(""" - select cost_center, balance, against_account, is_advance, - account_type, exchange_rate, account_currency - from `tabJournal Entry Account` where name = %s - """, d['voucher_detail_no'], as_dict=True) + jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0] + if flt(d['unadjusted_amount']) - flt(d['allocated_amount']) != 0: + # adjust the unreconciled balance amount_in_account_currency = flt(d['unadjusted_amount']) - flt(d['allocated_amount']) - amount_in_company_currency = amount_in_account_currency * flt(jvd[0]['exchange_rate']) + amount_in_company_currency = amount_in_account_currency * flt(jv_detail.exchange_rate) + jv_detail.set(d['dr_or_cr'], amount_in_account_currency) + jv_detail.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit', amount_in_company_currency) + else: + journal_entry.remove(jv_detail) - # new entry with balance amount - ch = jv_obj.append("accounts") - ch.account = d['account'] - ch.account_type = jvd[0]['account_type'] - ch.account_currency = jvd[0]['account_currency'] - ch.exchange_rate = jvd[0]['exchange_rate'] - ch.party_type = d["party_type"] - ch.party = d["party"] - ch.cost_center = cstr(jvd[0]["cost_center"]) - ch.balance = flt(jvd[0]["balance"]) + # new row with references + new_row = journal_entry.append("accounts") + new_row.update(jv_detail.as_dict().copy()) - ch.set(d['dr_or_cr'], amount_in_account_currency) - ch.set('debit' if d['dr_or_cr']=='debit_in_account_currency' else 'credit', amount_in_company_currency) + new_row.set(d["dr_or_cr"], d["allocated_amount"]) + new_row.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit', + d["allocated_amount"] * flt(jv_detail.exchange_rate)) - ch.set('credit_in_account_currency' if d['dr_or_cr']== 'debit_in_account_currency' - else 'debit_in_account_currency', 0) - ch.set('credit' if d['dr_or_cr']== 'debit_in_account_currency' else 'debit', 0) + new_row.set('credit_in_account_currency' if d['dr_or_cr'] == 'debit_in_account_currency' + else 'debit_in_account_currency', 0) + new_row.set('credit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'debit', 0) - ch.against_account = cstr(jvd[0]["against_account"]) - ch.reference_type = original_reference_type - ch.reference_name = original_reference_name - ch.is_advance = cstr(jvd[0]["is_advance"]) - ch.docstatus = 1 + new_row.set("reference_type", d["against_voucher_type"]) + new_row.set("reference_name", d["against_voucher"]) + + new_row.against_account = cstr(jv_detail.against_account) + new_row.is_advance = cstr(jv_detail.is_advance) + new_row.docstatus = 1 # will work as update after submit - jv_obj.flags.ignore_validate_update_after_submit = True - jv_obj.save(ignore_permissions=True) + journal_entry.flags.ignore_validate_update_after_submit = True + if not do_not_save: + journal_entry.save(ignore_permissions=True) def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): reference_details = { @@ -576,7 +576,7 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): @frappe.whitelist() def get_company_default(company, fieldname, ignore_validation=False): - value = frappe.get_cached_value('Company', company, fieldname) + value = frappe.get_cached_value('Company', company, fieldname) if not ignore_validation and not value: throw(_("Please set default {0} in Company {1}") @@ -1086,3 +1086,14 @@ def get_journal_entry(account, stock_adjustment_account, amount): db_or_cr_stock_adjustment_account : abs(amount) }] } + +def check_and_delete_linked_reports(report): + """ Check if reports are referenced in Desktop Icon """ + icons = frappe.get_all("Desktop Icon", + fields = ['name'], + filters = { + "_report": report + }) + if icons: + for icon in icons: + frappe.delete_doc("Desktop Icon", icon) diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json index b5bd14d137..2b26ac5090 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -233,6 +233,15 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Payment Reconciliation", + "link_to": "Payment Reconciliation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Sales Invoice", "hidden": 0, @@ -340,6 +349,15 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Payment Reconciliation", + "link_to": "Payment Reconciliation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Purchase Invoice", "hidden": 0, @@ -1188,7 +1206,7 @@ "type": "Link" } ], - "modified": "2021-08-05 12:15:52.872470", + "modified": "2021-08-27 12:15:52.872470", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", @@ -1249,4 +1267,4 @@ } ], "title": "Accounting" -} \ No newline at end of file +} diff --git a/erpnext/agriculture/doctype/agriculture_analysis_criteria/test_agriculture_analysis_criteria.js b/erpnext/agriculture/doctype/agriculture_analysis_criteria/test_agriculture_analysis_criteria.js deleted file mode 100644 index f70dcd2f32..0000000000 --- a/erpnext/agriculture/doctype/agriculture_analysis_criteria/test_agriculture_analysis_criteria.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Agriculture Analysis Criteria", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Agriculture Analysis Criteria - () => frappe.tests.make('Agriculture Analysis Criteria', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/agriculture/doctype/agriculture_task/test_agriculture_task.js b/erpnext/agriculture/doctype/agriculture_task/test_agriculture_task.js deleted file mode 100644 index a012c4b1ad..0000000000 --- a/erpnext/agriculture/doctype/agriculture_task/test_agriculture_task.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Agriculture Task", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Agriculture Task - () => frappe.tests.make('Agriculture Task', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/agriculture/doctype/plant_analysis/test_plant_analysis.js b/erpnext/agriculture/doctype/plant_analysis/test_plant_analysis.js deleted file mode 100644 index 786c0471a4..0000000000 --- a/erpnext/agriculture/doctype/plant_analysis/test_plant_analysis.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Plant Analysis", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Plant Analysis - () => frappe.tests.make('Plant Analysis', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/agriculture/doctype/soil_analysis/test_soil_analysis.js b/erpnext/agriculture/doctype/soil_analysis/test_soil_analysis.js deleted file mode 100644 index 29128eba27..0000000000 --- a/erpnext/agriculture/doctype/soil_analysis/test_soil_analysis.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Soil Analysis", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Soil Analysis - () => frappe.tests.make('Soil Analysis', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/agriculture/doctype/weather/test_weather.js b/erpnext/agriculture/doctype/weather/test_weather.js deleted file mode 100644 index b5009a4ccd..0000000000 --- a/erpnext/agriculture/doctype/weather/test_weather.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Weather", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Weather - () => frappe.tests.make('Weather', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/assets/doctype/asset/test_asset.js b/erpnext/assets/doctype/asset/test_asset.js deleted file mode 100644 index 6119e38217..0000000000 --- a/erpnext/assets/doctype/asset/test_asset.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Asset", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Asset - () => frappe.tests.make('Asset', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/assets/doctype/asset_category/test_asset_category.js b/erpnext/assets/doctype/asset_category/test_asset_category.js deleted file mode 100644 index 7e343b7519..0000000000 --- a/erpnext/assets/doctype/asset_category/test_asset_category.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Asset Category", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Asset Category - () => frappe.tests.make('Asset Category', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.js deleted file mode 100644 index f9b38a1020..0000000000 --- a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Asset Maintenance", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Asset Maintenance - () => frappe.tests.make('Asset Maintenance', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/assets/doctype/asset_maintenance_log/test_asset_maintenance_log.js b/erpnext/assets/doctype/asset_maintenance_log/test_asset_maintenance_log.js deleted file mode 100644 index 4e80184ea7..0000000000 --- a/erpnext/assets/doctype/asset_maintenance_log/test_asset_maintenance_log.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Asset Maintenance Log", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Asset Maintenance Log - () => frappe.tests.make('Asset Maintenance Log', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/assets/doctype/asset_maintenance_team/test_asset_maintenance_team.js b/erpnext/assets/doctype/asset_maintenance_team/test_asset_maintenance_team.js deleted file mode 100644 index 41bf69623e..0000000000 --- a/erpnext/assets/doctype/asset_maintenance_team/test_asset_maintenance_team.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Asset Maintenance Team", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Asset Maintenance Team - () => frappe.tests.make('Asset Maintenance Team', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.js b/erpnext/assets/doctype/asset_movement/test_asset_movement.js deleted file mode 100644 index b9515763c4..0000000000 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Asset Movement", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Asset Movement - () => frappe.tests.make('Asset Movement', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.js b/erpnext/assets/doctype/asset_repair/test_asset_repair.js deleted file mode 100644 index 7424ffe2b8..0000000000 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Asset Repair", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Asset Repair - () => frappe.tests.make('Asset Repair', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.js deleted file mode 100644 index 32831c61d2..0000000000 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Asset Value Adjustment", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Asset Value Adjustment - () => frappe.tests.make('Asset Value Adjustment', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/assets/doctype/location/test_location.js b/erpnext/assets/doctype/location/test_location.js deleted file mode 100644 index 3c06b63e82..0000000000 --- a/erpnext/assets/doctype/location/test_location.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Location", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Location - () => frappe.tests.make('Location', [ - // values to be set - { location_name: 'Basil Farm' } - ]), - () => { - assert.equal(cur_frm.doc.name, 'Basil Farm'); - }, - () => done() - ]); - -}); diff --git a/erpnext/assets/doctype/maintenance_team_member/test_maintenance_team_member.js b/erpnext/assets/doctype/maintenance_team_member/test_maintenance_team_member.js deleted file mode 100644 index d942e2a156..0000000000 --- a/erpnext/assets/doctype/maintenance_team_member/test_maintenance_team_member.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Maintenance Team Member", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Maintenance Team Member - () => frappe.tests.make('Maintenance Team Member', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/assets/form_tour/asset/asset.json b/erpnext/assets/form_tour/asset/asset.json new file mode 100644 index 0000000000..7c47a38bd1 --- /dev/null +++ b/erpnext/assets/form_tour/asset/asset.json @@ -0,0 +1,125 @@ +{ + "creation": "2021-08-24 16:55:10.923434", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-24 16:55:10.923434", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset", + "owner": "Administrator", + "reference_doctype": "Asset", + "save_on_complete": 0, + "steps": [ + { + "description": "Select Naming Series based on which Asset ID will be generated", + "field": "", + "fieldname": "naming_series", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Naming Series", + "parent_field": "", + "position": "Bottom", + "title": "Naming Series" + }, + { + "description": "Select an Asset Item", + "field": "", + "fieldname": "item_code", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Code", + "parent_field": "", + "position": "Bottom", + "title": "Item Code" + }, + { + "description": "Select a Location", + "field": "", + "fieldname": "location", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Location", + "parent_field": "", + "position": "Bottom", + "title": "Location" + }, + { + "description": "Check Is Existing Asset", + "field": "", + "fieldname": "is_existing_asset", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Is Existing Asset", + "parent_field": "", + "position": "Bottom", + "title": "Is Existing Asset?" + }, + { + "description": "Set Available for use date", + "field": "", + "fieldname": "available_for_use_date", + "fieldtype": "Date", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Available-for-use Date", + "parent_field": "", + "position": "Bottom", + "title": "Available For Use Date" + }, + { + "description": "Set Gross purchase amount", + "field": "", + "fieldname": "gross_purchase_amount", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Gross Purchase Amount", + "parent_field": "", + "position": "Bottom", + "title": "Gross Purchase Amount" + }, + { + "description": "Set Purchase Date", + "field": "", + "fieldname": "purchase_date", + "fieldtype": "Date", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Purchase Date", + "parent_field": "", + "position": "Bottom", + "title": "Purchase Date" + }, + { + "description": "Check Calculate Depreciation", + "field": "", + "fieldname": "calculate_depreciation", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Calculate Depreciation", + "parent_field": "", + "position": "Bottom", + "title": "Calculate Depreciation" + }, + { + "description": "Enter depreciation which has already been booked for this asset", + "field": "", + "fieldname": "opening_accumulated_depreciation", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Opening Accumulated Depreciation", + "parent_field": "", + "position": "Bottom", + "title": "Accumulated Depreciation" + } + ], + "title": "Asset" +} \ No newline at end of file diff --git a/erpnext/assets/form_tour/asset_category/asset_category.json b/erpnext/assets/form_tour/asset_category/asset_category.json new file mode 100644 index 0000000000..02834447a5 --- /dev/null +++ b/erpnext/assets/form_tour/asset_category/asset_category.json @@ -0,0 +1,65 @@ +{ + "creation": "2021-08-24 12:48:20.763173", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-24 12:48:20.763173", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Category", + "owner": "Administrator", + "reference_doctype": "Asset Category", + "save_on_complete": 0, + "steps": [ + { + "description": "Name Asset category. You can create categories based on Asset Types like Furniture, Property, Electronics etc.", + "field": "", + "fieldname": "asset_category_name", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Asset Category Name", + "parent_field": "", + "position": "Bottom", + "title": "Asset Category Name" + }, + { + "description": "Check to enable Capital Work in Progress accounting", + "field": "", + "fieldname": "enable_cwip_accounting", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Enable Capital Work in Progress Accounting", + "parent_field": "", + "position": "Bottom", + "title": "Enable CWIP Accounting" + }, + { + "description": "Add a row to define Depreciation Method and other details. Note that you can leave Finance Book blank to have it's accounting done in the primary books of accounts.", + "field": "", + "fieldname": "finance_books", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Finance Books", + "parent_field": "", + "position": "Bottom", + "title": "Finance Book Detail" + }, + { + "description": "Select the Fixed Asset and Depreciation accounts applicable for this Asset Category type", + "field": "", + "fieldname": "accounts", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Accounts", + "parent_field": "", + "position": "Bottom", + "title": "Accounts" + } + ], + "title": "Asset Category" +} \ No newline at end of file diff --git a/erpnext/assets/module_onboarding/assets/assets.json b/erpnext/assets/module_onboarding/assets/assets.json index 1086ab4bcd..e6df88b000 100644 --- a/erpnext/assets/module_onboarding/assets/assets.json +++ b/erpnext/assets/module_onboarding/assets/assets.json @@ -13,26 +13,26 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/asset", "idx": 0, "is_complete": 0, - "modified": "2020-07-08 14:05:51.828497", + "modified": "2021-08-24 17:50:41.573281", "modified_by": "Administrator", "module": "Assets", "name": "Assets", "owner": "Administrator", "steps": [ { - "step": "Introduction to Assets" + "step": "Fixed Asset Accounts" }, { - "step": "Create a Fixed Asset Item" + "step": "Asset Category" }, { - "step": "Create an Asset Category" + "step": "Asset Item" }, { - "step": "Purchase an Asset Item" + "step": "Asset Purchase" }, { - "step": "Create an Asset" + "step": "Existing Asset" } ], "subtitle": "Assets, Depreciations, Repairs, and more.", diff --git a/erpnext/assets/onboarding_step/asset_category/asset_category.json b/erpnext/assets/onboarding_step/asset_category/asset_category.json new file mode 100644 index 0000000000..033e86669c --- /dev/null +++ b/erpnext/assets/onboarding_step/asset_category/asset_category.json @@ -0,0 +1,21 @@ +{ + "action": "Show Form Tour", + "action_label": "Let's review existing Asset Category", + "creation": "2021-08-13 14:26:18.656303", + "description": "# Asset Category\n\nAn Asset Category classifies different assets of a Company.\n\nYou can create an Asset Category based on the type of assets. For example, all your desktops and laptops can be part of an Asset Category named \"Electronic Equipments\". Create a separate category for furniture. Also, you can update default properties for each category, like:\n - Depreciation type and duration\n - Fixed asset account\n - Depreciation account\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-24 12:49:37.665239", + "modified_by": "Administrator", + "name": "Asset Category", + "owner": "Administrator", + "reference_document": "Asset Category", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Define Asset Category", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/asset_item/asset_item.json b/erpnext/assets/onboarding_step/asset_item/asset_item.json new file mode 100644 index 0000000000..8a174c5b77 --- /dev/null +++ b/erpnext/assets/onboarding_step/asset_item/asset_item.json @@ -0,0 +1,21 @@ +{ + "action": "Show Form Tour", + "action_label": "Let's create a new Asset item", + "creation": "2021-08-13 14:27:07.277167", + "description": "# Asset Item\n\nAsset items are created based on Asset Category. You can create one or multiple items against once Asset Category. The sales and purchase transaction for Asset is done via Asset Item. ", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-16 13:59:18.362233", + "modified_by": "Administrator", + "name": "Asset Item", + "owner": "Administrator", + "reference_document": "Item", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Create an Asset Item", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json new file mode 100644 index 0000000000..54611edc29 --- /dev/null +++ b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json @@ -0,0 +1,21 @@ +{ + "action": "Show Form Tour", + "action_label": "Let's create a Purchase Receipt", + "creation": "2021-08-13 14:27:53.678621", + "description": "# Purchase an Asset\n\nAssets purchases process if done following the standard Purchase cycle. If capital work in progress is enabled in Asset Category, Asset will be created as soon as Purchase Receipt is created for it. You can quickly create a Purchase Receipt for Asset and see its impact on books of accounts.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-24 17:26:57.180637", + "modified_by": "Administrator", + "name": "Asset Purchase", + "owner": "Administrator", + "reference_document": "Purchase Receipt", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Purchase an Asset", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/existing_asset/existing_asset.json b/erpnext/assets/onboarding_step/existing_asset/existing_asset.json new file mode 100644 index 0000000000..052d5e8d77 --- /dev/null +++ b/erpnext/assets/onboarding_step/existing_asset/existing_asset.json @@ -0,0 +1,21 @@ +{ + "action": "Show Form Tour", + "action_label": "Add an existing Asset", + "creation": "2021-08-13 14:28:30.650459", + "description": "# Add an Existing Asset\n\nIf you are just starting with ERPNext, you will need to enter Assets you already possess. You can add them as existing fixed assets in ERPNext. Please note that you will have to make a Journal Entry separately updating the opening balance in the fixed asset account.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-16 14:03:48.850471", + "modified_by": "Administrator", + "name": "Existing Asset", + "owner": "Administrator", + "reference_document": "Asset", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Add an Existing Asset", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json new file mode 100644 index 0000000000..cebee7a7ea --- /dev/null +++ b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json @@ -0,0 +1,21 @@ +{ + "action": "Go to Page", + "action_label": "Let's walk-through Chart of Accounts to review setup", + "creation": "2021-08-13 14:23:09.297765", + "description": "# Fixed Asset Accounts\n\nWith the company, a host of fixed asset accounts are pre-configured. To ensure your asset transactions are leading to correct accounting entries, you can review and set up following asset accounts as per your business requirements.\n - Fixed asset accounts (Asset account)\n - Accumulated depreciation\n - Capital Work in progress (CWIP) account\n - Asset Depreciation account (Expense account)", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-24 17:46:37.646174", + "modified_by": "Administrator", + "name": "Fixed Asset Accounts", + "owner": "Administrator", + "path": "app/account/view/tree", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Review Fixed Asset Accounts", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 1766c2c80c..7ee91961ca 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -24,7 +24,26 @@ frappe.ui.form.on("Supplier", { } } }); + + frm.set_query("supplier_primary_contact", function(doc) { + return { + query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact", + filters: { + "supplier": doc.name + } + }; + }); + + frm.set_query("supplier_primary_address", function(doc) { + return { + filters: { + "link_doctype": "Supplier", + "link_name": doc.name + } + }; + }); }, + refresh: function (frm) { frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Supplier' } @@ -78,6 +97,30 @@ frappe.ui.form.on("Supplier", { }); }, + supplier_primary_address: function(frm) { + if (frm.doc.supplier_primary_address) { + frappe.call({ + method: 'frappe.contacts.doctype.address.address.get_address_display', + args: { + "address_dict": frm.doc.supplier_primary_address + }, + callback: function(r) { + frm.set_value("primary_address", r.message); + } + }); + } + if (!frm.doc.supplier_primary_address) { + frm.set_value("primary_address", ""); + } + }, + + supplier_primary_contact: function(frm) { + if (!frm.doc.supplier_primary_contact) { + frm.set_value("mobile_no", ""); + frm.set_value("email_id", ""); + } + }, + is_internal_supplier: function(frm) { if (frm.doc.is_internal_supplier == 1) { frm.toggle_reqd("represents_company", true); diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 38b8dfdf48..c7a5db5994 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -49,6 +49,13 @@ "address_html", "column_break1", "contact_html", + "primary_address_and_contact_detail_section", + "supplier_primary_contact", + "mobile_no", + "email_id", + "column_break_44", + "supplier_primary_address", + "primary_address", "default_payable_accounts", "accounts", "default_tax_withholding_config", @@ -378,6 +385,47 @@ "fieldname": "allow_purchase_invoice_creation_without_purchase_receipt", "fieldtype": "Check", "label": "Allow Purchase Invoice Creation Without Purchase Receipt" + }, + { + "fieldname": "primary_address_and_contact_detail_section", + "fieldtype": "Section Break", + "label": "Primary Address and Contact Detail" + }, + { + "description": "Reselect, if the chosen contact is edited after save", + "fieldname": "supplier_primary_contact", + "fieldtype": "Link", + "label": "Supplier Primary Contact", + "options": "Contact" + }, + { + "fetch_from": "supplier_primary_contact.mobile_no", + "fieldname": "mobile_no", + "fieldtype": "Read Only", + "label": "Mobile No" + }, + { + "fetch_from": "supplier_primary_contact.email_id", + "fieldname": "email_id", + "fieldtype": "Read Only", + "label": "Email Id" + }, + { + "fieldname": "column_break_44", + "fieldtype": "Column Break" + }, + { + "fieldname": "primary_address", + "fieldtype": "Text", + "label": "Primary Address", + "read_only": 1 + }, + { + "description": "Reselect, if the chosen address is edited after save", + "fieldname": "supplier_primary_address", + "fieldtype": "Link", + "label": "Supplier Primary Address", + "options": "Address" } ], "icon": "fa fa-user", @@ -390,7 +438,7 @@ "link_fieldname": "supplier" } ], - "modified": "2021-05-18 15:10:11.087191", + "modified": "2021-08-27 18:02:44.314077", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index fd16b23c22..c9750caa65 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -42,7 +42,12 @@ class Supplier(TransactionBase): if not self.naming_series: self.naming_series = '' + self.create_primary_contact() + self.create_primary_address() + def validate(self): + self.flags.is_new_doc = self.is_new() + # validation for Naming Series mandatory field... if frappe.defaults.get_global_default('supp_master_name') == 'Naming Series': if not self.naming_series: @@ -76,7 +81,39 @@ class Supplier(TransactionBase): frappe.throw(_("Internal Supplier for company {0} already exists").format( frappe.bold(self.represents_company))) + def create_primary_contact(self): + from erpnext.selling.doctype.customer.customer import make_contact + + if not self.supplier_primary_contact: + if self.mobile_no or self.email_id: + contact = make_contact(self) + self.db_set('supplier_primary_contact', contact.name) + self.db_set('mobile_no', self.mobile_no) + self.db_set('email_id', self.email_id) + + def create_primary_address(self): + from erpnext.selling.doctype.customer.customer import make_address + from frappe.contacts.doctype.address.address import get_address_display + + if self.flags.is_new_doc and self.get('address_line1'): + address = make_address(self) + address_display = get_address_display(address.name) + + self.db_set("supplier_primary_address", address.name) + self.db_set("primary_address", address_display) + def on_trash(self): + if self.supplier_primary_contact: + frappe.db.sql(""" + UPDATE `tabSupplier` + SET + supplier_primary_contact=null, + supplier_primary_address=null, + mobile_no=null, + email_id=null, + primary_address=null + WHERE name=%(name)s""", {"name": self.name}) + delete_contact_and_address('Supplier', self.name) def after_rename(self, olddn, newdn, merge=False): @@ -104,3 +141,21 @@ class Supplier(TransactionBase): doc.name, args.get('supplier_email_' + str(i))) except frappe.NameError: pass + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters): + supplier = filters.get("supplier") + return frappe.db.sql(""" + SELECT + `tabContact`.name from `tabContact`, + `tabDynamic Link` + WHERE + `tabContact`.name = `tabDynamic Link`.parent + and `tabDynamic Link`.link_name = %(supplier)s + and `tabDynamic Link`.link_doctype = 'Supplier' + and `tabContact`.name like %(txt)s + """, { + 'supplier': supplier, + 'txt': '%%%s%%' % txt + }) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 9f82af9b37..fc5dc098e5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -14,7 +14,7 @@ from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_a from erpnext.utilities.transaction_base import TransactionBase from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.sales_and_purchase_return import validate_return -from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled +from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled, get_party_account from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_transaction, apply_pricing_rule_for_free_items, get_applied_pricing_rules) from erpnext.exceptions import InvalidCurrency @@ -1206,7 +1206,7 @@ class AccountsController(TransactionBase): d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount')) d.outstanding = d.payment_amount elif not d.invoice_portion: - d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount')) + d.base_payment_amount = flt(d.payment_amount * self.get("conversion_rate"), d.precision('base_payment_amount')) def get_order_details(self): @@ -1363,6 +1363,67 @@ class AccountsController(TransactionBase): return False + def process_common_party_accounting(self): + is_invoice = self.doctype in ['Sales Invoice', 'Purchase Invoice'] + if not is_invoice: + return + + if frappe.db.get_single_value('Accounts Settings', 'enable_common_party_accounting'): + party_link = self.get_common_party_link() + if party_link and self.outstanding_amount: + self.create_advance_and_reconcile(party_link) + + def get_common_party_link(self): + party_type, party = self.get_party() + return frappe.db.get_value( + doctype='Party Link', + filters={'secondary_role': party_type, 'secondary_party': party}, + fieldname=['primary_role', 'primary_party'], + as_dict=True + ) + + def create_advance_and_reconcile(self, party_link): + secondary_party_type, secondary_party = self.get_party() + primary_party_type, primary_party = party_link.primary_role, party_link.primary_party + + primary_account = get_party_account(primary_party_type, primary_party, self.company) + secondary_account = get_party_account(secondary_party_type, secondary_party, self.company) + + jv = frappe.new_doc('Journal Entry') + jv.voucher_type = 'Journal Entry' + jv.posting_date = self.posting_date + jv.company = self.company + jv.remark = 'Adjustment for {} {}'.format(self.doctype, self.name) + + reconcilation_entry = frappe._dict() + advance_entry = frappe._dict() + + reconcilation_entry.account = secondary_account + reconcilation_entry.party_type = secondary_party_type + reconcilation_entry.party = secondary_party + reconcilation_entry.reference_type = self.doctype + reconcilation_entry.reference_name = self.name + reconcilation_entry.cost_center = self.cost_center + + advance_entry.account = primary_account + advance_entry.party_type = primary_party_type + advance_entry.party = primary_party + advance_entry.cost_center = self.cost_center + advance_entry.is_advance = 'Yes' + + if self.doctype == 'Sales Invoice': + reconcilation_entry.credit_in_account_currency = self.outstanding_amount + advance_entry.debit_in_account_currency = self.outstanding_amount + else: + advance_entry.credit_in_account_currency = self.outstanding_amount + reconcilation_entry.debit_in_account_currency = self.outstanding_amount + + jv.append('accounts', reconcilation_entry) + jv.append('accounts', advance_entry) + + jv.save() + jv.submit() + @frappe.whitelist() def get_tax_rate(account_head): return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) @@ -1526,7 +1587,7 @@ def get_advance_journal_entries(party_type, party, party_account, amount_field, def get_advance_payment_entries(party_type, party, party_account, order_doctype, - order_list=None, include_unallocated=True, against_all_orders=False, limit=None): + order_list=None, include_unallocated=True, against_all_orders=False, limit=None, condition=None): party_account_field = "paid_from" if party_type == "Customer" else "paid_to" currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" payment_type = "Receive" if party_type == "Customer" else "Pay" @@ -1561,14 +1622,14 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, if include_unallocated: unallocated_payment_entries = frappe.db.sql(""" - select "Payment Entry" as reference_type, name as reference_name, - remarks, unallocated_amount as amount, {2} as exchange_rate + select "Payment Entry" as reference_type, name as reference_name, posting_date, + remarks, unallocated_amount as amount, {2} as exchange_rate, {3} as currency from `tabPayment Entry` where {0} = %s and party_type = %s and party = %s and payment_type = %s - and docstatus = 1 and unallocated_amount > 0 + and docstatus = 1 and unallocated_amount > 0 {condition} order by posting_date {1} - """.format(party_account_field, limit_cond, exchange_rate_field), + """.format(party_account_field, limit_cond, exchange_rate_field, currency_field, condition=condition or ""), (party_account, party_type, party, payment_type), as_dict=1) return list(payment_entries_against_order) + list(unallocated_payment_entries) diff --git a/erpnext/controllers/employee_boarding_controller.py b/erpnext/controllers/employee_boarding_controller.py index 1898222916..f43c80416f 100644 --- a/erpnext/controllers/employee_boarding_controller.py +++ b/erpnext/controllers/employee_boarding_controller.py @@ -5,7 +5,9 @@ import frappe from frappe import _ from frappe.desk.form import assign_to from frappe.model.document import Document -from frappe.utils import flt, unique +from frappe.utils import flt, unique, add_days +from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday +from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee class EmployeeBoardingController(Document): ''' @@ -41,10 +43,14 @@ class EmployeeBoardingController(Document): def create_task_and_notify_user(self): # create the task for the given project and assign to the concerned person + holiday_list = self.get_holiday_list() + for activity in self.activities: if activity.task: continue + dates = self.get_task_dates(activity, holiday_list) + task = frappe.get_doc({ 'doctype': 'Task', 'project': self.project, @@ -52,7 +58,9 @@ class EmployeeBoardingController(Document): 'description': activity.description, 'department': self.department, 'company': self.company, - 'task_weight': activity.task_weight + 'task_weight': activity.task_weight, + 'exp_start_date': dates[0], + 'exp_end_date': dates[1] }).insert(ignore_permissions=True) activity.db_set('task', task.name) @@ -79,6 +87,36 @@ class EmployeeBoardingController(Document): if users: self.assign_task_to_users(task, users) + def get_holiday_list(self): + if self.doctype == 'Employee Separation': + return get_holiday_list_for_employee(self.employee) + else: + if self.employee: + return get_holiday_list_for_employee(self.employee) + else: + if not self.holiday_list: + frappe.throw(_('Please set the Holiday List.'), frappe.MandatoryError) + else: + return self.holiday_list + + def get_task_dates(self, activity, holiday_list): + start_date = end_date = None + + if activity.begin_on: + start_date = add_days(self.boarding_begins_on, activity.begin_on) + start_date = self.update_if_holiday(start_date, holiday_list) + + if activity.duration: + end_date = add_days(self.boarding_begins_on, activity.begin_on + activity.duration) + end_date = self.update_if_holiday(end_date, holiday_list) + + return [start_date, end_date] + + def update_if_holiday(self, date, holiday_list): + while is_holiday(holiday_list, date): + date = add_days(date, 1) + return date + def assign_task_to_users(self, task, users): for user in users: args = { @@ -103,7 +141,8 @@ class EmployeeBoardingController(Document): @frappe.whitelist() def get_onboarding_details(parent, parenttype): return frappe.get_all('Employee Boarding Activity', - fields=['activity_name', 'role', 'user', 'required_for_employee_creation', 'description', 'task_weight'], + fields=['activity_name', 'role', 'user', 'required_for_employee_creation', + 'description', 'task_weight', 'begin_on', 'duration'], filters={'parent': parent, 'parenttype': parenttype}, order_by= 'idx') diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 5ee1f2f7fb..01486fcd65 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -394,19 +394,6 @@ def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None if not return_against: return_against = frappe.get_cached_value(voucher_type, voucher_no, "return_against") - if not return_against and voucher_type == 'Sales Invoice' and sle: - return get_incoming_rate({ - "item_code": sle.item_code, - "warehouse": sle.warehouse, - "posting_date": sle.get('posting_date'), - "posting_time": sle.get('posting_time'), - "qty": sle.actual_qty, - "serial_no": sle.get('serial_no'), - "company": sle.company, - "voucher_type": sle.voucher_type, - "voucher_no": sle.voucher_no - }, raise_error_if_no_rate=False) - return_against_item_field = get_return_against_item_fields(voucher_type) filters = get_filters(voucher_type, voucher_no, voucher_detail_no, @@ -417,7 +404,24 @@ def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None else: select_field = "abs(stock_value_difference / actual_qty)" - return flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field)) + rate = flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field)) + if not (rate and return_against) and voucher_type in ['Sales Invoice', 'Delivery Note']: + rate = frappe.db.get_value(f'{voucher_type} Item', voucher_detail_no, 'incoming_rate') + + if not rate and sle: + rate = get_incoming_rate({ + "item_code": sle.item_code, + "warehouse": sle.warehouse, + "posting_date": sle.get('posting_date'), + "posting_time": sle.get('posting_time'), + "qty": sle.actual_qty, + "serial_no": sle.get('serial_no'), + "company": sle.company, + "voucher_type": sle.voucher_type, + "voucher_no": sle.voucher_no + }, raise_error_if_no_rate=False) + + return rate def get_return_against_item_fields(voucher_type): return_against_item_fields = { diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index fc2cc97e0a..844c40c8a6 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -362,7 +362,7 @@ class SellingController(StockController): sales_order.update_reserved_qty(so_item_rows) def set_incoming_rate(self): - if self.doctype not in ("Delivery Note", "Sales Invoice", "Sales Order"): + if self.doctype not in ("Delivery Note", "Sales Invoice"): return items = self.get("items") + (self.get("packed_items") or []) @@ -371,18 +371,19 @@ class SellingController(StockController): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get('stock_qty') or d.get('actual_qty')) - d.incoming_rate = get_incoming_rate({ - "item_code": d.item_code, - "warehouse": d.warehouse, - "posting_date": self.get('posting_date') or self.get('transaction_date'), - "posting_time": self.get('posting_time') or nowtime(), - "qty": qty if cint(self.get("is_return")) else (-1 * qty), - "serial_no": d.get('serial_no'), - "company": self.company, - "voucher_type": self.doctype, - "voucher_no": self.name, - "allow_zero_valuation": d.get("allow_zero_valuation") - }, raise_error_if_no_rate=False) + if not d.incoming_rate: + d.incoming_rate = get_incoming_rate({ + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": self.get('posting_date') or self.get('transaction_date'), + "posting_time": self.get('posting_time') or nowtime(), + "qty": qty if cint(self.get("is_return")) else (-1 * qty), + "serial_no": d.get('serial_no'), + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + "allow_zero_valuation": d.get("allow_zero_valuation") + }, raise_error_if_no_rate=False) # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): @@ -422,7 +423,7 @@ class SellingController(StockController): or (cint(self.is_return) and self.docstatus==2)): sl_entries.append(self.get_sle_for_source_warehouse(d)) - if d.target_warehouse: + if d.target_warehouse and self.get("is_internal_customer"): sl_entries.append(self.get_sle_for_target_warehouse(d)) if d.warehouse and ((not cint(self.is_return) and self.docstatus==2) diff --git a/erpnext/crm/doctype/contract/test_contract.js b/erpnext/crm/doctype/contract/test_contract.js deleted file mode 100644 index 4c77c3d649..0000000000 --- a/erpnext/crm/doctype/contract/test_contract.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Contract", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Contract - () => frappe.tests.make('Contract', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/crm/doctype/contract_fulfilment_checklist/test_contract_fulfilment_checklist.js b/erpnext/crm/doctype/contract_fulfilment_checklist/test_contract_fulfilment_checklist.js deleted file mode 100644 index 2a2d5e1bfc..0000000000 --- a/erpnext/crm/doctype/contract_fulfilment_checklist/test_contract_fulfilment_checklist.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Contract Fulfilment Checklist", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Contract Fulfilment Checklist - () => frappe.tests.make('Contract Fulfilment Checklist', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/crm/doctype/contract_template/test_contract_template.js b/erpnext/crm/doctype/contract_template/test_contract_template.js deleted file mode 100644 index 6aaddd7df4..0000000000 --- a/erpnext/crm/doctype/contract_template/test_contract_template.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Contract Template", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Contract Template - () => frappe.tests.make('Contract Template', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index 75af937990..95cf03241b 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -39,6 +39,8 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create")); this.frm.add_custom_button(__("Opportunity"), this.make_opportunity, __("Create")); this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create")); + this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create")); + this.frm.add_custom_button(__('Add to Prospect'), this.add_lead_to_prospect, __('Action')); } if (!this.frm.is_new()) { @@ -49,27 +51,74 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller } } - make_customer () { + add_lead_to_prospect (frm) { + frappe.prompt([ + { + fieldname: 'prospect', + label: __('Prospect'), + fieldtype: 'Link', + options: 'Prospect', + reqd: 1 + } + ], + function(data) { + frappe.call({ + method: 'erpnext.crm.doctype.lead.lead.add_lead_to_prospect', + args: { + 'lead': frm.doc.name, + 'prospect': data.prospect + }, + callback: function(r) { + if (!r.exc) { + frm.reload_doc(); + } + }, + freeze: true, + freeze_message: __('...Adding Lead to Prospect') + }); + }, __('Add Lead to Prospect'), __('Add')); + } + + make_customer (frm) { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_customer", - frm: cur_frm + frm: frm }) } - make_opportunity () { + make_opportunity (frm) { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_opportunity", - frm: cur_frm + frm: frm }) } - make_quotation () { + make_quotation (frm) { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_quotation", - frm: cur_frm + frm: frm }) } + make_prospect (frm) { + frappe.model.with_doctype("Prospect", function() { + let prospect = frappe.model.get_new_doc("Prospect"); + prospect.company_name = frm.doc.company_name; + prospect.no_of_employees = frm.doc.no_of_employees; + prospect.industry = frm.doc.industry; + prospect.market_segment = frm.doc.market_segment; + prospect.territory = frm.doc.territory; + prospect.fax = frm.doc.fax; + prospect.website = frm.doc.website; + prospect.prospect_owner = frm.doc.lead_owner; + + let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead'); + lead_prospect_row.lead = frm.doc.name; + + frappe.set_route("Form", "Prospect", prospect.name); + }); + } + company_name () { if (!this.frm.doc.lead_name) { this.frm.set_value("lead_name", this.frm.doc.company_name); diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index ebec699e70..09dbdb9dad 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -63,6 +63,7 @@ class Lead(SellingController): def on_update(self): self.add_calendar_event() + self.update_prospects() def before_insert(self): self.contact_doc = self.create_contact() @@ -89,6 +90,12 @@ class Lead(SellingController): "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') }, force) + def update_prospects(self): + prospects = frappe.get_all('Prospect Lead', filters={'lead': self.name}, fields=['parent']) + for row in prospects: + prospect = frappe.get_doc('Prospect', row.parent) + prospect.save(ignore_permissions=True) + def check_email_id_is_unique(self): if self.email_id: # validate email is unique @@ -355,3 +362,14 @@ def daily_open_lead(): leads = frappe.get_all("Lead", filters = [["contact_date", "Between", [nowdate(), nowdate()]]]) for lead in leads: frappe.db.set_value("Lead", lead.name, "status", "Open") + +@frappe.whitelist() +def add_lead_to_prospect(lead, prospect): + prospect = frappe.get_doc('Prospect', prospect) + prospect.append('prospect_lead', { + 'lead': lead + }) + prospect.save(ignore_permissions=True) + frappe.msgprint(_('Lead {0} has been added to prospect {1}.').format(frappe.bold(lead), frappe.bold(prospect.name)), + title=_('Lead Added'), indicator='green') + \ No newline at end of file diff --git a/erpnext/crm/doctype/lead/lead_dashboard.py b/erpnext/crm/doctype/lead/lead_dashboard.py index 3950d063f2..50e88a5188 100644 --- a/erpnext/crm/doctype/lead/lead_dashboard.py +++ b/erpnext/crm/doctype/lead/lead_dashboard.py @@ -13,7 +13,7 @@ def get_data(): }, 'transactions': [ { - 'items': ['Opportunity', 'Quotation'] + 'items': ['Opportunity', 'Quotation', 'Prospect'] }, ] } diff --git a/erpnext/crm/doctype/lead/lead_list.js b/erpnext/crm/doctype/lead/lead_list.js new file mode 100644 index 0000000000..75208fa64b --- /dev/null +++ b/erpnext/crm/doctype/lead/lead_list.js @@ -0,0 +1,28 @@ +frappe.listview_settings['Lead'] = { + onload: function(listview) { + if (frappe.boot.user.can_create.includes("Prospect")) { + listview.page.add_action_item(__("Create Prospect"), function() { + frappe.model.with_doctype("Prospect", function() { + let prospect = frappe.model.get_new_doc("Prospect"); + let leads = listview.get_checked_items(); + frappe.db.get_value("Lead", leads[0].name, ["company_name", "no_of_employees", "industry", "market_segment", "territory", "fax", "website", "lead_owner"], (r) => { + prospect.company_name = r.company_name; + prospect.no_of_employees = r.no_of_employees; + prospect.industry = r.industry; + prospect.market_segment = r.market_segment; + prospect.territory = r.territory; + prospect.fax = r.fax; + prospect.website = r.website; + prospect.prospect_owner = r.lead_owner; + + leads.forEach(function(lead) { + let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead'); + lead_prospect_row.lead = lead.name; + }); + frappe.set_route("Form", "Prospect", prospect.name); + }); + }); + }); + } + } +}; diff --git a/erpnext/crm/doctype/market_segment/test_market_segment.js b/erpnext/crm/doctype/market_segment/test_market_segment.js deleted file mode 100644 index aa4b868f93..0000000000 --- a/erpnext/crm/doctype/market_segment/test_market_segment.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Market Segment", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Market Segment - () => frappe.tests.make('Market Segment', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index cb95881cb4..3866fc263e 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -10,12 +10,12 @@ frappe.ui.form.on("Opportunity", { frm.custom_make_buttons = { 'Quotation': 'Quotation', 'Supplier Quotation': 'Supplier Quotation' - }, + }; frm.set_query("opportunity_from", function() { return{ "filters": { - "name": ["in", ["Customer", "Lead"]], + "name": ["in", ["Customer", "Lead", "Prospect"]], } } }); diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 4ba4140244..12a564a9cb 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -430,7 +430,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2021-06-04 10:11:22.831139", + "modified": "2021-08-25 10:28:24.923543", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/crm/doctype/opportunity_type/test_opportunity_type.js b/erpnext/crm/doctype/opportunity_type/test_opportunity_type.js deleted file mode 100644 index 3a1ede94db..0000000000 --- a/erpnext/crm/doctype/opportunity_type/test_opportunity_type.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Opportunity Type", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Opportunity Type - () => frappe.tests.make('Opportunity Type', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/crm/doctype/prospect/__init__.py b/erpnext/crm/doctype/prospect/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/prospect/prospect.js b/erpnext/crm/doctype/prospect/prospect.js new file mode 100644 index 0000000000..67018e1ef9 --- /dev/null +++ b/erpnext/crm/doctype/prospect/prospect.js @@ -0,0 +1,29 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Prospect', { + refresh (frm) { + if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) { + frm.add_custom_button(__("Customer"), function() { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.prospect.prospect.make_customer", + frm: frm + }); + }, __("Create")); + } + if (!frm.is_new() && frappe.boot.user.can_create.includes("Opportunity")) { + frm.add_custom_button(__("Opportunity"), function() { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.prospect.prospect.make_opportunity", + frm: frm + }); + }, __("Create")); + } + + if (!frm.is_new()) { + frappe.contacts.render_address_and_contact(frm); + } else { + frappe.contacts.clear_address_and_contact(frm); + } + } +}); diff --git a/erpnext/crm/doctype/prospect/prospect.json b/erpnext/crm/doctype/prospect/prospect.json new file mode 100644 index 0000000000..3d6fba5123 --- /dev/null +++ b/erpnext/crm/doctype/prospect/prospect.json @@ -0,0 +1,203 @@ +{ + "actions": [], + "autoname": "field:company_name", + "creation": "2021-08-19 00:21:06.995448", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company_name", + "industry", + "market_segment", + "customer_group", + "territory", + "column_break_6", + "no_of_employees", + "currency", + "annual_revenue", + "more_details_section", + "fax", + "website", + "column_break_13", + "prospect_owner", + "leads_section", + "prospect_lead", + "address_and_contact_section", + "address_html", + "column_break_17", + "contact_html", + "notes_section", + "notes" + ], + "fields": [ + { + "fieldname": "company_name", + "fieldtype": "Data", + "label": "Company Name", + "unique": 1 + }, + { + "fieldname": "industry", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Industry", + "options": "Industry Type" + }, + { + "fieldname": "market_segment", + "fieldtype": "Link", + "label": "Market Segment", + "options": "Market Segment" + }, + { + "fieldname": "customer_group", + "fieldtype": "Link", + "label": "Customer Group", + "options": "Customer Group" + }, + { + "fieldname": "territory", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Territory", + "options": "Territory" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "no_of_employees", + "fieldtype": "Int", + "label": "No. of Employees" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "annual_revenue", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Annual Revenue", + "options": "currency" + }, + { + "fieldname": "fax", + "fieldtype": "Data", + "label": "Fax", + "options": "Phone" + }, + { + "fieldname": "website", + "fieldtype": "Data", + "label": "Website", + "options": "URL" + }, + { + "fieldname": "prospect_owner", + "fieldtype": "Link", + "label": "Prospect Owner", + "options": "User" + }, + { + "fieldname": "leads_section", + "fieldtype": "Section Break", + "label": "Leads" + }, + { + "fieldname": "prospect_lead", + "fieldtype": "Table", + "options": "Prospect Lead" + }, + { + "fieldname": "address_html", + "fieldtype": "HTML", + "label": "Address HTML" + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" + }, + { + "fieldname": "contact_html", + "fieldtype": "HTML", + "label": "Contact HTML" + }, + { + "collapsible": 1, + "fieldname": "notes_section", + "fieldtype": "Section Break", + "label": "Notes" + }, + { + "fieldname": "notes", + "fieldtype": "Text Editor" + }, + { + "fieldname": "more_details_section", + "fieldtype": "Section Break", + "label": "More Details" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: !doc.__islocal", + "fieldname": "address_and_contact_section", + "fieldtype": "Section Break", + "label": "Address and Contact" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-08-27 16:24:42.961967", + "modified_by": "Administrator", + "module": "CRM", + "name": "Prospect", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "company_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/prospect/prospect.py b/erpnext/crm/doctype/prospect/prospect.py new file mode 100644 index 0000000000..5f5815de5e --- /dev/null +++ b/erpnext/crm/doctype/prospect/prospect.py @@ -0,0 +1,99 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc +from frappe.contacts.address_and_contact import load_address_and_contact + +class Prospect(Document): + def onload(self): + load_address_and_contact(self) + + def validate(self): + self.update_lead_details() + + def on_update(self): + self.link_with_lead_contact_and_address() + + def on_trash(self): + self.unlink_dynamic_links() + + def update_lead_details(self): + for row in self.get('prospect_lead'): + lead = frappe.get_value('Lead', row.lead, ['lead_name', 'status', 'email_id', 'mobile_no'], as_dict=True) + row.lead_name = lead.lead_name + row.status = lead.status + row.email = lead.email_id + row.mobile_no = lead.mobile_no + + def link_with_lead_contact_and_address(self): + for row in self.prospect_lead: + links = frappe.get_all('Dynamic Link', filters={'link_doctype': 'Lead', 'link_name': row.lead}, fields=['parent', 'parenttype']) + for link in links: + linked_doc = frappe.get_doc(link['parenttype'], link['parent']) + exists = False + + for d in linked_doc.get('links'): + if d.link_doctype == self.doctype and d.link_name == self.name: + exists = True + + if not exists: + linked_doc.append('links', { + 'link_doctype': self.doctype, + 'link_name': self.name + }) + linked_doc.save(ignore_permissions=True) + + def unlink_dynamic_links(self): + links = frappe.get_all('Dynamic Link', filters={'link_doctype': self.doctype, 'link_name': self.name}, fields=['parent', 'parenttype']) + + for link in links: + linked_doc = frappe.get_doc(link['parenttype'], link['parent']) + + if len(linked_doc.get('links')) == 1: + linked_doc.delete(ignore_permissions=True) + else: + to_remove = None + for d in linked_doc.get('links'): + if d.link_doctype == self.doctype and d.link_name == self.name: + to_remove = d + if to_remove: + linked_doc.remove(to_remove) + linked_doc.save(ignore_permissions=True) + +@frappe.whitelist() +def make_customer(source_name, target_doc=None): + def set_missing_values(source, target): + target.customer_type = "Company" + target.company_name = source.name + target.customer_group = source.customer_group or frappe.db.get_default("Customer Group") + + doclist = get_mapped_doc("Prospect", source_name, + {"Prospect": { + "doctype": "Customer", + "field_map": { + "company_name": "customer_name", + "currency": "default_currency", + "fax": "fax" + } + }}, target_doc, set_missing_values, ignore_permissions=False) + + return doclist + +@frappe.whitelist() +def make_opportunity(source_name, target_doc=None): + def set_missing_values(source, target): + target.opportunity_from = "Prospect" + target.customer_name = source.company_name + target.customer_group = source.customer_group or frappe.db.get_default("Customer Group") + + doclist = get_mapped_doc("Prospect", source_name, + {"Prospect": { + "doctype": "Opportunity", + "field_map": { + "name": "party_name", + } + }}, target_doc, set_missing_values, ignore_permissions=False) + + return doclist diff --git a/erpnext/crm/doctype/prospect/test_prospect.py b/erpnext/crm/doctype/prospect/test_prospect.py new file mode 100644 index 0000000000..0fffad1939 --- /dev/null +++ b/erpnext/crm/doctype/prospect/test_prospect.py @@ -0,0 +1,54 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +import unittest +from frappe.utils import random_string +from erpnext.crm.doctype.lead.test_lead import make_lead +from erpnext.crm.doctype.lead.lead import add_lead_to_prospect + + +class TestProspect(unittest.TestCase): + def test_add_lead_to_prospect_and_address_linking(self): + lead_doc = make_lead() + address_doc = make_address(address_title=lead_doc.name) + address_doc.append('links', { + "link_doctype": lead_doc.doctype, + "link_name": lead_doc.name + }) + address_doc.save() + prospect_doc = make_prospect() + add_lead_to_prospect(lead_doc.name, prospect_doc.name) + prospect_doc.reload() + lead_exists_in_prosoect = False + for rec in prospect_doc.get('prospect_lead'): + if rec.lead == lead_doc.name: + lead_exists_in_prosoect = True + self.assertEqual(lead_exists_in_prosoect, True) + address_doc.reload() + self.assertEqual(address_doc.has_link('Prospect', prospect_doc.name), True) + + +def make_prospect(**args): + args = frappe._dict(args) + + prospect_doc = frappe.get_doc({ + "doctype": "Prospect", + "company_name": args.company_name or "_Test Company {}".format(random_string(3)), + }).insert() + + return prospect_doc + +def make_address(**args): + args = frappe._dict(args) + + address_doc = frappe.get_doc({ + "doctype": "Address", + "address_title": args.address_title or "Address Title", + "address_type": args.address_type or "Billing", + "city": args.city or "Mumbai", + "address_line1": args.address_line1 or "Vidya Vihar West", + "country": args.country or "India" + }).insert() + + return address_doc diff --git a/erpnext/crm/doctype/prospect_lead/__init__.py b/erpnext/crm/doctype/prospect_lead/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/prospect_lead/prospect_lead.json b/erpnext/crm/doctype/prospect_lead/prospect_lead.json new file mode 100644 index 0000000000..3c160d9e80 --- /dev/null +++ b/erpnext/crm/doctype/prospect_lead/prospect_lead.json @@ -0,0 +1,67 @@ +{ + "actions": [], + "creation": "2021-08-19 00:14:14.857421", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "lead", + "lead_name", + "status", + "email", + "mobile_no" + ], + "fields": [ + { + "fieldname": "lead", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Lead", + "options": "Lead", + "reqd": 1 + }, + { + "fieldname": "lead_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Lead Name", + "read_only": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Lead\nOpen\nReplied\nOpportunity\nQuotation\nLost Quotation\nInterested\nConverted\nDo Not Contact", + "read_only": 1 + }, + { + "fieldname": "email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Email", + "options": "Email", + "read_only": 1 + }, + { + "fieldname": "mobile_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Mobile No", + "options": "Phone", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-08-25 12:58:24.638054", + "modified_by": "Administrator", + "module": "CRM", + "name": "Prospect Lead", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/prospect_lead/prospect_lead.py b/erpnext/crm/doctype/prospect_lead/prospect_lead.py new file mode 100644 index 0000000000..2be5a5f39a --- /dev/null +++ b/erpnext/crm/doctype/prospect_lead/prospect_lead.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class ProspectLead(Document): + pass diff --git a/erpnext/crm/doctype/sales_stage/test_sales_stage.js b/erpnext/crm/doctype/sales_stage/test_sales_stage.js deleted file mode 100644 index 807af1fd98..0000000000 --- a/erpnext/crm/doctype/sales_stage/test_sales_stage.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Sales Stage", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Sales Stage - () => frappe.tests.make('Sales Stage', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/academic_year/test_academic_year.js b/erpnext/education/doctype/academic_year/test_academic_year.js deleted file mode 100644 index 51e9cf307d..0000000000 --- a/erpnext/education/doctype/academic_year/test_academic_year.js +++ /dev/null @@ -1,23 +0,0 @@ -// Testing Setup Module in Education -QUnit.module('education'); - -QUnit.test('Test: Academic Year', function(assert){ - assert.expect(3); - let done = assert.async(); - frappe.run_serially([ - () => { - return frappe.tests.make('Academic Year', [ - {academic_year_name: '2016-17'}, - {year_start_date: '2016-07-20'}, - {year_end_date:'2017-06-20'}, - ]); - }, - - () => { - assert.ok(cur_frm.doc.academic_year_name=='2016-17'); - assert.ok(cur_frm.doc.year_start_date=='2016-07-20'); - assert.ok(cur_frm.doc.year_end_date=='2017-06-20'); - }, - () => done() - ]); -}); diff --git a/erpnext/education/doctype/article/test_article.js b/erpnext/education/doctype/article/test_article.js deleted file mode 100644 index 9dbf063e84..0000000000 --- a/erpnext/education/doctype/article/test_article.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Article", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Article - () => frappe.tests.make('Article', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/content_question/test_content_question.js b/erpnext/education/doctype/content_question/test_content_question.js deleted file mode 100644 index cc869a87fc..0000000000 --- a/erpnext/education/doctype/content_question/test_content_question.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Content Question", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Content Question - () => frappe.tests.make('Content Question', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/course_activity/test_course_activity.js b/erpnext/education/doctype/course_activity/test_course_activity.js deleted file mode 100644 index c89c89e5d3..0000000000 --- a/erpnext/education/doctype/course_activity/test_course_activity.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Course Activity", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Course Activity - () => frappe.tests.make('Course Activity', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/course_content/test_course_content.js b/erpnext/education/doctype/course_content/test_course_content.js deleted file mode 100644 index 786e67e9a3..0000000000 --- a/erpnext/education/doctype/course_content/test_course_content.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Course Content", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Course Content - () => frappe.tests.make('Course Content', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/course_enrollment/test_course_enrollment.js b/erpnext/education/doctype/course_enrollment/test_course_enrollment.js deleted file mode 100644 index 216cc30799..0000000000 --- a/erpnext/education/doctype/course_enrollment/test_course_enrollment.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Course Enrollment", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Course Enrollment - () => frappe.tests.make('Course Enrollment', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/course_schedule/test_course_schedule.js b/erpnext/education/doctype/course_schedule/test_course_schedule.js deleted file mode 100644 index 5cdb67be48..0000000000 --- a/erpnext/education/doctype/course_schedule/test_course_schedule.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Course Schedule", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Course Schedule - () => frappe.tests.make('Course Schedule', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/course_scheduling_tool/test_course_scheduling_tool.js b/erpnext/education/doctype/course_scheduling_tool/test_course_scheduling_tool.js deleted file mode 100644 index 4419d18116..0000000000 --- a/erpnext/education/doctype/course_scheduling_tool/test_course_scheduling_tool.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Course Scheduling Tool", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Course Scheduling Tool - () => frappe.tests.make('Course Scheduling Tool', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/course_topic/test_course_topic.js b/erpnext/education/doctype/course_topic/test_course_topic.js deleted file mode 100644 index d8d154fb9c..0000000000 --- a/erpnext/education/doctype/course_topic/test_course_topic.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Course Topic", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Course Topic - () => frappe.tests.make('Course Topic', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/fee_category/test_fee_category.js b/erpnext/education/doctype/fee_category/test_fee_category.js deleted file mode 100644 index a08ed33e8b..0000000000 --- a/erpnext/education/doctype/fee_category/test_fee_category.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Fee Category", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Fee Category - () => frappe.tests.make('Fee Category', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/fee_schedule/test_fee_schedule.js b/erpnext/education/doctype/fee_schedule/test_fee_schedule.js deleted file mode 100644 index d495b4ce7b..0000000000 --- a/erpnext/education/doctype/fee_schedule/test_fee_schedule.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Fee Schedule", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('Fee Schedule', [ - // insert a new Fee Schedule - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/fee_structure/test_fee_structure.js b/erpnext/education/doctype/fee_structure/test_fee_structure.js deleted file mode 100644 index 61f41354c3..0000000000 --- a/erpnext/education/doctype/fee_structure/test_fee_structure.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Fee Structure", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Fee Structure - () => frappe.tests.make('Fee Structure', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/program_enrollment/test_program_enrollment.js b/erpnext/education/doctype/program_enrollment/test_program_enrollment.js deleted file mode 100644 index aea81a0714..0000000000 --- a/erpnext/education/doctype/program_enrollment/test_program_enrollment.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Program Enrollment", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Program Enrollment - () => frappe.tests.make('Program Enrollment', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/program_enrollment_tool/test_program_enrollment_tool.js b/erpnext/education/doctype/program_enrollment_tool/test_program_enrollment_tool.js deleted file mode 100644 index 8d55104a0f..0000000000 --- a/erpnext/education/doctype/program_enrollment_tool/test_program_enrollment_tool.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Program Enrollment Tool", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Program Enrollment Tool - () => frappe.tests.make('Program Enrollment Tool', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/question/test_question.js b/erpnext/education/doctype/question/test_question.js deleted file mode 100644 index 509939c6b5..0000000000 --- a/erpnext/education/doctype/question/test_question.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Question", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Question - () => frappe.tests.make('Question', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/quiz/test_quiz.js b/erpnext/education/doctype/quiz/test_quiz.js deleted file mode 100644 index 147d13952a..0000000000 --- a/erpnext/education/doctype/quiz/test_quiz.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Quiz", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Quiz - () => frappe.tests.make('Quiz', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/quiz_activity/test_quiz_activity.js b/erpnext/education/doctype/quiz_activity/test_quiz_activity.js deleted file mode 100644 index 94b5ab796a..0000000000 --- a/erpnext/education/doctype/quiz_activity/test_quiz_activity.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Quiz Activity", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Quiz Activity - () => frappe.tests.make('Quiz Activity', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/quiz_result/test_quiz_result.js b/erpnext/education/doctype/quiz_result/test_quiz_result.js deleted file mode 100644 index 43f53a1dc7..0000000000 --- a/erpnext/education/doctype/quiz_result/test_quiz_result.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Quiz Result", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Quiz Result - () => frappe.tests.make('Quiz Result', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/school_house/test_school_house.js b/erpnext/education/doctype/school_house/test_school_house.js deleted file mode 100644 index dde63ecc4c..0000000000 --- a/erpnext/education/doctype/school_house/test_school_house.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: School House", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new School House - () => frappe.tests.make('School House', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/student/test_student.js b/erpnext/education/doctype/student/test_student.js deleted file mode 100644 index e18d39aee0..0000000000 --- a/erpnext/education/doctype/student/test_student.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Student", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Student - () => frappe.tests.make('Student', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/student_language/test_student_language.js b/erpnext/education/doctype/student_language/test_student_language.js deleted file mode 100644 index 9b25569961..0000000000 --- a/erpnext/education/doctype/student_language/test_student_language.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Student Language", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Student Language - () => frappe.tests.make('Student Language', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/student_report_generation_tool/test_student_report_generation_tool.js b/erpnext/education/doctype/student_report_generation_tool/test_student_report_generation_tool.js deleted file mode 100644 index 10be092bb9..0000000000 --- a/erpnext/education/doctype/student_report_generation_tool/test_student_report_generation_tool.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Student Report Generation Tool", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Student Report Generation Tool - () => frappe.tests.make('Student Report Generation Tool', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/topic/test_topic.js b/erpnext/education/doctype/topic/test_topic.js deleted file mode 100644 index 4460b79478..0000000000 --- a/erpnext/education/doctype/topic/test_topic.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Topic", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Topic - () => frappe.tests.make('Topic', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/education/doctype/topic_content/test_topic_content.js b/erpnext/education/doctype/topic_content/test_topic_content.js deleted file mode 100644 index bf9a62d037..0000000000 --- a/erpnext/education/doctype/topic_content/test_topic_content.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Topic Content", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Topic Content - () => frappe.tests.make('Topic Content', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.js b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.js deleted file mode 100644 index 9c8990986e..0000000000 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Amazon MWS Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Amazon MWS Settings - () => frappe.tests.make('Amazon MWS Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/erpnext_integrations/doctype/gocardless_mandate/test_gocardless_mandate.js b/erpnext/erpnext_integrations/doctype/gocardless_mandate/test_gocardless_mandate.js deleted file mode 100644 index caa9399eb6..0000000000 --- a/erpnext/erpnext_integrations/doctype/gocardless_mandate/test_gocardless_mandate.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: GoCardless Mandate", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new GoCardless Mandate - () => frappe.tests.make('GoCardless Mandate', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/test_gocardless_settings.js b/erpnext/erpnext_integrations/doctype/gocardless_settings/test_gocardless_settings.js deleted file mode 100644 index b6daad8de4..0000000000 --- a/erpnext/erpnext_integrations/doctype/gocardless_settings/test_gocardless_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: GoCardless Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new GoCardless Settings - () => frappe.tests.make('GoCardless Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.js deleted file mode 100644 index dc91347336..0000000000 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Plaid Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Plaid Settings - () => frappe.tests.make('Plaid Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.js b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.js deleted file mode 100644 index b71d704807..0000000000 --- a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: QuickBooks Migrator", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new QuickBooks Migrator - () => frappe.tests.make('QuickBooks Migrator', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/test_tally_migration.js b/erpnext/erpnext_integrations/doctype/tally_migration/test_tally_migration.js deleted file mode 100644 index 433c5e2cda..0000000000 --- a/erpnext/erpnext_integrations/doctype/tally_migration/test_tally_migration.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Tally Migration", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Tally Migration - () => frappe.tests.make('Tally Migration', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.js b/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.js deleted file mode 100644 index ea06ab2dc4..0000000000 --- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Woocommerce Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Woocommerce Settings - () => frappe.tests.make('Woocommerce Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/erpnext_integrations/stripe_integration.py b/erpnext/erpnext_integrations/stripe_integration.py index 108b4c0dd8..820c740532 100644 --- a/erpnext/erpnext_integrations/stripe_integration.py +++ b/erpnext/erpnext_integrations/stripe_integration.py @@ -2,11 +2,12 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals +import stripe + import frappe from frappe import _ from frappe.integrations.utils import create_request_log -import stripe + def create_stripe_subscription(gateway_controller, data): stripe_settings = frappe.get_doc("Stripe Settings", gateway_controller) @@ -23,31 +24,38 @@ def create_stripe_subscription(gateway_controller, data): except Exception: frappe.log_error(frappe.get_traceback()) return{ - "redirect_to": frappe.redirect_to_message(_('Server Error'), _("It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account.")), + "redirect_to": frappe.redirect_to_message( + _('Server Error'), + _("It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account.") + ), "status": 401 } def create_subscription_on_stripe(stripe_settings): - items = [] - for payment_plan in stripe_settings.payment_plans: - plan = frappe.db.get_value("Subscription Plan", payment_plan.plan, "payment_plan_id") - items.append({"plan": plan, "quantity": payment_plan.qty}) + items = [] + for payment_plan in stripe_settings.payment_plans: + plan = frappe.db.get_value("Subscription Plan", payment_plan.plan, "product_price_id") + items.append({"price": plan, "quantity": payment_plan.qty}) - try: - customer = stripe.Customer.create(description=stripe_settings.data.payer_name, email=stripe_settings.data.payer_email, source=stripe_settings.data.stripe_token_id) - subscription = stripe.Subscription.create(customer=customer, items=items) + try: + customer = stripe.Customer.create( + source=stripe_settings.data.stripe_token_id, + description=stripe_settings.data.payer_name, + email=stripe_settings.data.payer_email + ) - if subscription.status == "active": - stripe_settings.integration_request.db_set('status', 'Completed', update_modified=False) - stripe_settings.flags.status_changed_to = "Completed" + subscription = stripe.Subscription.create(customer=customer, items=items) - else: - stripe_settings.integration_request.db_set('status', 'Failed', update_modified=False) - frappe.log_error('Subscription N°: ' + subscription.id, 'Stripe Payment not completed') + if subscription.status == "active": + stripe_settings.integration_request.db_set('status', 'Completed', update_modified=False) + stripe_settings.flags.status_changed_to = "Completed" - except Exception: + else: stripe_settings.integration_request.db_set('status', 'Failed', update_modified=False) - frappe.log_error(frappe.get_traceback()) + frappe.log_error('Subscription N°: ' + subscription.id, 'Stripe Payment not completed') + except Exception: + stripe_settings.integration_request.db_set('status', 'Failed', update_modified=False) + frappe.log_error(frappe.get_traceback()) - return stripe_settings.finalize_request() + return stripe_settings.finalize_request() diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py index f960998c3c..83764ae50d 100644 --- a/erpnext/erpnext_integrations/taxjar_integration.py +++ b/erpnext/erpnext_integrations/taxjar_integration.py @@ -1,11 +1,10 @@ import traceback - -import taxjar - import frappe +import taxjar from erpnext import get_default_company from frappe import _ from frappe.contacts.doctype.address.address import get_company_address +from frappe.utils import cint TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head") @@ -14,6 +13,10 @@ TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_cal SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO", "SE", "SI", "SK", "US"] +SUPPORTED_STATE_CODES = ['AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'DC', 'FL', 'GA', 'HI', 'ID', 'IL', + 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', + 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD', + 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY'] def get_client(): @@ -27,7 +30,11 @@ def get_client(): api_url = taxjar.SANDBOX_API_URL if api_key and api_url: - return taxjar.Client(api_key=api_key, api_url=api_url) + client = taxjar.Client(api_key=api_key, api_url=api_url) + client.set_api_config('headers', { + 'x-api-version': '2020-08-07' + }) + return client def create_transaction(doc, method): @@ -57,7 +64,10 @@ def create_transaction(doc, method): tax_dict['amount'] = doc.total + tax_dict['shipping'] try: - client.create_order(tax_dict) + if doc.is_return: + client.create_refund(tax_dict) + else: + client.create_order(tax_dict) except taxjar.exceptions.TaxJarResponseError as err: frappe.throw(_(sanitize_error_response(err))) except Exception as ex: @@ -89,14 +99,16 @@ def get_tax_data(doc): to_country_code = frappe.db.get_value("Country", to_address.country, "code") to_country_code = to_country_code.upper() - if to_country_code not in SUPPORTED_COUNTRY_CODES: - return - shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD]) - if to_shipping_state is not None: - to_shipping_state = get_iso_3166_2_state_code(to_address) + line_items = [get_line_item_dict(item) for item in doc.items] + if from_shipping_state not in SUPPORTED_STATE_CODES: + from_shipping_state = get_state_code(from_address, 'Company') + + if to_shipping_state not in SUPPORTED_STATE_CODES: + to_shipping_state = get_state_code(to_address, 'Shipping') + tax_dict = { 'from_country': from_country_code, 'from_zip': from_address.pincode, @@ -109,11 +121,29 @@ def get_tax_data(doc): 'to_street': to_address.address_line1, 'to_state': to_shipping_state, 'shipping': shipping, - 'amount': doc.net_total + 'amount': doc.net_total, + 'plugin': 'erpnext', + 'line_items': line_items } + return tax_dict - return tax_dict +def get_state_code(address, location): + if address is not None: + state_code = get_iso_3166_2_state_code(address) + if state_code not in SUPPORTED_STATE_CODES: + frappe.throw(_("Please enter a valid State in the {0} Address").format(location)) + else: + frappe.throw(_("Please enter a valid State in the {0} Address").format(location)) + + return state_code +def get_line_item_dict(item): + return dict( + id = item.get('idx'), + quantity = item.get('qty'), + unit_price = item.get('rate'), + product_tax_code = item.get('product_tax_category') + ) def set_sales_tax(doc, method): if not TAXJAR_CALCULATE_TAX: @@ -122,17 +152,7 @@ def set_sales_tax(doc, method): if not doc.items: return - # if the party is exempt from sales tax, then set all tax account heads to zero - sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \ - or frappe.db.has_column("Customer", "exempt_from_sales_tax") and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax") - - if sales_tax_exempted: - for tax in doc.taxes: - if tax.account_head == TAX_ACCOUNT_HEAD: - tax.tax_amount = 0 - break - - doc.run_method("calculate_taxes_and_totals") + if check_sales_tax_exemption(doc): return tax_dict = get_tax_data(doc) @@ -143,7 +163,6 @@ def set_sales_tax(doc, method): return tax_data = validate_tax_request(tax_dict) - if tax_data is not None: if not tax_data.amount_to_collect: setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD]) @@ -163,9 +182,28 @@ def set_sales_tax(doc, method): "account_head": TAX_ACCOUNT_HEAD, "tax_amount": tax_data.amount_to_collect }) + # Assigning values to tax_collectable and taxable_amount fields in sales item table + for item in tax_data.breakdown.line_items: + doc.get('items')[cint(item.id)-1].tax_collectable = item.tax_collectable + doc.get('items')[cint(item.id)-1].taxable_amount = item.taxable_amount doc.run_method("calculate_taxes_and_totals") +def check_sales_tax_exemption(doc): + # if the party is exempt from sales tax, then set all tax account heads to zero + sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \ + or frappe.db.has_column("Customer", "exempt_from_sales_tax") \ + and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax") + + if sales_tax_exempted: + for tax in doc.taxes: + if tax.account_head == TAX_ACCOUNT_HEAD: + tax.tax_amount = 0 + break + doc.run_method("calculate_taxes_and_totals") + return True + else: + return False def validate_tax_request(tax_dict): """Return the sales tax that should be collected for a given order.""" @@ -200,6 +238,8 @@ def get_shipping_address_details(doc): if doc.shipping_address_name: shipping_address = frappe.get_doc("Address", doc.shipping_address_name) + elif doc.customer_address: + shipping_address = frappe.get_doc("Address", doc.customer_address_name) else: shipping_address = get_company_address_details(doc) diff --git a/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json b/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json index a59f149ee5..6803528156 100644 --- a/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json +++ b/erpnext/healthcare/dashboard_chart/clinical_procedures/clinical_procedures.json @@ -12,15 +12,15 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:47.008622", - "modified": "2020-07-22 13:36:48.114479", + "last_synced_on": "2021-01-30 21:03:30.086891", + "modified": "2021-02-01 13:36:04.469863", "modified_by": "Administrator", "module": "Healthcare", "name": "Clinical Procedures", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Percentage", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart/clinical_procedures_status/clinical_procedures_status.json b/erpnext/healthcare/dashboard_chart/clinical_procedures_status/clinical_procedures_status.json index 6d560f74bf..dae9db19b8 100644 --- a/erpnext/healthcare/dashboard_chart/clinical_procedures_status/clinical_procedures_status.json +++ b/erpnext/healthcare/dashboard_chart/clinical_procedures_status/clinical_procedures_status.json @@ -12,15 +12,15 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:46.691764", - "modified": "2020-07-22 13:40:17.215775", + "last_synced_on": "2021-02-01 13:36:38.787783", + "modified": "2021-02-01 13:37:18.718275", "modified_by": "Administrator", "module": "Healthcare", "name": "Clinical Procedures Status", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Pie", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart/diagnoses/diagnoses.json b/erpnext/healthcare/dashboard_chart/diagnoses/diagnoses.json index 0195aac8b7..82145d6024 100644 --- a/erpnext/healthcare/dashboard_chart/diagnoses/diagnoses.json +++ b/erpnext/healthcare/dashboard_chart/diagnoses/diagnoses.json @@ -5,21 +5,22 @@ "docstatus": 0, "doctype": "Dashboard Chart", "document_type": "Patient Encounter Diagnosis", + "dynamic_filters_json": "", "filters_json": "[]", "group_by_based_on": "diagnosis", "group_by_type": "Count", "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:47.895521", - "modified": "2020-07-22 13:43:32.369481", + "last_synced_on": "2021-01-30 21:03:33.729487", + "modified": "2021-02-01 13:34:57.385335", "modified_by": "Administrator", "module": "Healthcare", "name": "Diagnoses", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Percentage", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart/lab_tests/lab_tests.json b/erpnext/healthcare/dashboard_chart/lab_tests/lab_tests.json index 052483533e..70293b158e 100644 --- a/erpnext/healthcare/dashboard_chart/lab_tests/lab_tests.json +++ b/erpnext/healthcare/dashboard_chart/lab_tests/lab_tests.json @@ -12,15 +12,15 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:47.344055", - "modified": "2020-07-22 13:37:34.490129", + "last_synced_on": "2021-01-30 21:03:28.272914", + "modified": "2021-02-01 13:36:08.391433", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Tests", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Percentage", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart/symptoms/symptoms.json b/erpnext/healthcare/dashboard_chart/symptoms/symptoms.json index 8fc86a1c59..65e5472aa1 100644 --- a/erpnext/healthcare/dashboard_chart/symptoms/symptoms.json +++ b/erpnext/healthcare/dashboard_chart/symptoms/symptoms.json @@ -12,15 +12,15 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 13:22:47.296748", - "modified": "2020-07-22 13:40:59.655129", + "last_synced_on": "2021-01-30 21:03:32.067473", + "modified": "2021-02-01 13:35:30.953718", "modified_by": "Administrator", "module": "Healthcare", "name": "Symptoms", "number_of_groups": 0, "owner": "Administrator", "timeseries": 0, - "type": "Percentage", + "type": "Bar", "use_report_chart": 0, "y_axis": [] } \ No newline at end of file diff --git a/erpnext/healthcare/doctype/antibiotic/test_antibiotic.js b/erpnext/healthcare/doctype/antibiotic/test_antibiotic.js deleted file mode 100644 index b92103d750..0000000000 --- a/erpnext/healthcare/doctype/antibiotic/test_antibiotic.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Antibiotic", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Antibiotic - () => frappe.tests.make('Antibiotic', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/appointment_type/test_appointment_type.js b/erpnext/healthcare/doctype/appointment_type/test_appointment_type.js deleted file mode 100644 index 93274e55c7..0000000000 --- a/erpnext/healthcare/doctype/appointment_type/test_appointment_type.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Appointment Type", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Appointment Type - () => frappe.tests.make('Appointment Type', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.js deleted file mode 100644 index 80ef3d55f2..0000000000 --- a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Clinical Procedure", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Clinical Procedure - () => frappe.tests.make('Clinical Procedure', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py index 81a3982c4b..0326e5e9da 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py @@ -11,7 +11,7 @@ test_dependencies = ['Item'] class TestClinicalProcedure(unittest.TestCase): def test_procedure_template_item(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() procedure_template = create_clinical_procedure_template() self.assertTrue(frappe.db.exists('Item', procedure_template.item)) @@ -20,7 +20,7 @@ class TestClinicalProcedure(unittest.TestCase): self.assertEqual(frappe.db.get_value('Item', procedure_template.item, 'disabled'), 1) def test_consumables(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() procedure_template = create_clinical_procedure_template() procedure_template.allow_stock_consumption = 1 consumable = create_consumable() diff --git a/erpnext/healthcare/doctype/clinical_procedure_template/test_clinical_procedure_template.js b/erpnext/healthcare/doctype/clinical_procedure_template/test_clinical_procedure_template.js deleted file mode 100644 index 1dde8b5d86..0000000000 --- a/erpnext/healthcare/doctype/clinical_procedure_template/test_clinical_procedure_template.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Clinical Procedure Template", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Clinical Procedure Template - () => frappe.tests.make('Clinical Procedure Template', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/complaint/test_complaint.js b/erpnext/healthcare/doctype/complaint/test_complaint.js deleted file mode 100644 index 9ff44d8da4..0000000000 --- a/erpnext/healthcare/doctype/complaint/test_complaint.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Complaint", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Complaint - () => frappe.tests.make('Complaint', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/diagnosis/test_diagnosis.js b/erpnext/healthcare/doctype/diagnosis/test_diagnosis.js deleted file mode 100644 index cacfef5b17..0000000000 --- a/erpnext/healthcare/doctype/diagnosis/test_diagnosis.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Diagnosis", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Diagnosis - () => frappe.tests.make('Diagnosis', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/dosage_form/test_dosage_form.js b/erpnext/healthcare/doctype/dosage_form/test_dosage_form.js deleted file mode 100644 index ba54ab16fa..0000000000 --- a/erpnext/healthcare/doctype/dosage_form/test_dosage_form.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Dosage Form", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Dosage Form - () => frappe.tests.make('Dosage Form', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/drug_prescription/drug_prescription.json b/erpnext/healthcare/doctype/drug_prescription/drug_prescription.json index d91e6bf9dc..a65c56694e 100644 --- a/erpnext/healthcare/doctype/drug_prescription/drug_prescription.json +++ b/erpnext/healthcare/doctype/drug_prescription/drug_prescription.json @@ -56,6 +56,7 @@ "reqd": 1 }, { + "allow_in_quick_entry": 1, "fieldname": "dosage_form", "fieldtype": "Link", "ignore_user_permissions": 1, @@ -109,7 +110,7 @@ ], "istable": 1, "links": [], - "modified": "2020-09-30 23:32:09.495288", + "modified": "2021-06-11 11:53:06.343704", "modified_by": "Administrator", "module": "Healthcare", "name": "Drug Prescription", diff --git a/erpnext/healthcare/doctype/fee_validity/fee_validity.json b/erpnext/healthcare/doctype/fee_validity/fee_validity.json index b001bf024c..d76b42e683 100644 --- a/erpnext/healthcare/doctype/fee_validity/fee_validity.json +++ b/erpnext/healthcare/doctype/fee_validity/fee_validity.json @@ -46,13 +46,13 @@ { "fieldname": "visited", "fieldtype": "Int", - "label": "Visited yet", + "label": "Visits Completed", "read_only": 1 }, { "fieldname": "valid_till", "fieldtype": "Date", - "label": "Valid till", + "label": "Valid Till", "read_only": 1 }, { @@ -106,7 +106,7 @@ ], "in_create": 1, "links": [], - "modified": "2020-03-17 20:25:06.487418", + "modified": "2021-08-26 10:51:05.609349", "modified_by": "Administrator", "module": "Healthcare", "name": "Fee Validity", diff --git a/erpnext/healthcare/doctype/fee_validity/fee_validity.py b/erpnext/healthcare/doctype/fee_validity/fee_validity.py index 5b9c17934f..59586e0c31 100644 --- a/erpnext/healthcare/doctype/fee_validity/fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/fee_validity.py @@ -11,7 +11,6 @@ import datetime class FeeValidity(Document): def validate(self): self.update_status() - self.set_start_date() def update_status(self): if self.visited >= self.max_visits: @@ -19,13 +18,6 @@ class FeeValidity(Document): else: self.status = 'Pending' - def set_start_date(self): - self.start_date = getdate() - for appointment in self.ref_appointments: - appointment_date = frappe.db.get_value('Patient Appointment', appointment.appointment, 'appointment_date') - if getdate(appointment_date) < self.start_date: - self.start_date = getdate(appointment_date) - def create_fee_validity(appointment): if not check_is_new_patient(appointment): @@ -36,11 +28,9 @@ def create_fee_validity(appointment): fee_validity.patient = appointment.patient fee_validity.max_visits = frappe.db.get_single_value('Healthcare Settings', 'max_visits') or 1 valid_days = frappe.db.get_single_value('Healthcare Settings', 'valid_days') or 1 - fee_validity.visited = 1 + fee_validity.visited = 0 + fee_validity.start_date = getdate(appointment.appointment_date) fee_validity.valid_till = getdate(appointment.appointment_date) + datetime.timedelta(days=int(valid_days)) - fee_validity.append('ref_appointments', { - 'appointment': appointment.name - }) fee_validity.save(ignore_permissions=True) return fee_validity diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.js b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.js deleted file mode 100644 index 0ebb97438c..0000000000 --- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Fee Validity", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Fee Validity - () => frappe.tests.make('Fee Validity', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py index 82e7136d6b..957f85211d 100644 --- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py @@ -22,14 +22,14 @@ class TestFeeValidity(unittest.TestCase): item = create_healthcare_service_items() healthcare_settings = frappe.get_single("Healthcare Settings") healthcare_settings.enable_free_follow_ups = 1 - healthcare_settings.max_visits = 2 + healthcare_settings.max_visits = 1 healthcare_settings.valid_days = 7 healthcare_settings.automate_appointment_invoicing = 1 healthcare_settings.op_consulting_charge_item = item healthcare_settings.save(ignore_permissions=True) - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() - # For first appointment, invoice is generated + # For first appointment, invoice is generated. First appointment not considered in fee validity appointment = create_appointment(patient, practitioner, nowdate()) invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced") self.assertEqual(invoiced, 1) diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/test_healthcare_practitioner.js b/erpnext/healthcare/doctype/healthcare_practitioner/test_healthcare_practitioner.js deleted file mode 100644 index 75aa208ec1..0000000000 --- a/erpnext/healthcare/doctype/healthcare_practitioner/test_healthcare_practitioner.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Healthcare Practitioner", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Healthcare Practitioner - () => frappe.tests.make('Healthcare Practitioner', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.js b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.js index 2cdd550656..2d1caf7efc 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.js +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.js @@ -7,8 +7,8 @@ frappe.ui.form.on('Healthcare Service Unit', { // get query select healthcare service unit frm.fields_dict['parent_healthcare_service_unit'].get_query = function(doc) { - return{ - filters:[ + return { + filters: [ ['Healthcare Service Unit', 'is_group', '=', 1], ['Healthcare Service Unit', 'name', '!=', doc.healthcare_service_unit_name] ] @@ -21,6 +21,14 @@ frappe.ui.form.on('Healthcare Service Unit', { frm.add_custom_button(__('Healthcare Service Unit Tree'), function() { frappe.set_route('Tree', 'Healthcare Service Unit'); }); + + frm.set_query('warehouse', function() { + return { + filters: { + 'company': frm.doc.company + } + }; + }); }, set_root_readonly: function(frm) { // read-only for root healthcare service unit @@ -43,5 +51,10 @@ frappe.ui.form.on('Healthcare Service Unit', { else { frm.set_df_property('service_unit_type', 'reqd', 1); } + }, + overlap_appointments: function(frm) { + if (frm.doc.overlap_appointments == 0) { + frm.set_value('service_unit_capacity', ''); + } } }); diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json index 9ee865a62a..8935ec7d3c 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json @@ -16,6 +16,7 @@ "service_unit_type", "allow_appointments", "overlap_appointments", + "service_unit_capacity", "inpatient_occupancy", "occupancy_status", "column_break_9", @@ -31,6 +32,8 @@ { "fieldname": "healthcare_service_unit_name", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "in_global_search": 1, "in_list_view": 1, "label": "Service Unit", @@ -41,6 +44,8 @@ "bold": 1, "fieldname": "parent_healthcare_service_unit", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "in_list_view": 1, "label": "Parent Service Unit", @@ -52,6 +57,8 @@ "depends_on": "eval:doc.inpatient_occupancy != 1 && doc.allow_appointments != 1", "fieldname": "is_group", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Is Group" }, { @@ -59,6 +66,8 @@ "depends_on": "eval:doc.is_group != 1", "fieldname": "service_unit_type", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Service Unit Type", "options": "Healthcare Service Unit Type" }, @@ -68,6 +77,8 @@ "fetch_from": "service_unit_type.allow_appointments", "fieldname": "allow_appointments", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Allow Appointments", "no_copy": 1, @@ -79,6 +90,8 @@ "fetch_from": "service_unit_type.overlap_appointments", "fieldname": "overlap_appointments", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Allow Overlap", "no_copy": 1, "read_only": 1 @@ -90,6 +103,8 @@ "fetch_from": "service_unit_type.inpatient_occupancy", "fieldname": "inpatient_occupancy", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Inpatient Occupancy", "no_copy": 1, @@ -100,6 +115,8 @@ "depends_on": "eval:doc.inpatient_occupancy == 1", "fieldname": "occupancy_status", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Occupancy Status", "no_copy": 1, "options": "Vacant\nOccupied", @@ -107,13 +124,17 @@ }, { "fieldname": "column_break_9", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "bold": 1, "depends_on": "eval:doc.is_group != 1", "fieldname": "warehouse", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Warehouse", "no_copy": 1, "options": "Warehouse" @@ -121,6 +142,8 @@ { "fieldname": "company", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "in_list_view": 1, "in_standard_filter": 1, @@ -134,6 +157,8 @@ "fieldname": "lft", "fieldtype": "Int", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "lft", "no_copy": 1, "print_hide": 1, @@ -143,6 +168,8 @@ "fieldname": "rgt", "fieldtype": "Int", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "rgt", "no_copy": 1, "print_hide": 1, @@ -152,6 +179,8 @@ "fieldname": "old_parent", "fieldtype": "Link", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "label": "Old Parent", "no_copy": 1, @@ -163,14 +192,26 @@ "collapsible": 1, "fieldname": "tree_details_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Tree Details" + }, + { + "depends_on": "eval:doc.overlap_appointments == 1", + "fieldname": "service_unit_capacity", + "fieldtype": "Int", + "label": "Service Unit Capacity", + "mandatory_depends_on": "eval:doc.overlap_appointments == 1", + "non_negative": 1 } ], + "is_tree": 1, "links": [], - "modified": "2020-05-20 18:26:56.065543", + "modified": "2021-08-19 14:09:11.643464", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Service Unit", + "nsm_parent_field": "parent_healthcare_service_unit", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py index 9e0417a2be..5e76ed7284 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py @@ -5,14 +5,21 @@ from __future__ import unicode_literals from frappe.utils.nestedset import NestedSet +from frappe.utils import cint, cstr import frappe +from frappe import _ +import json + class HealthcareServiceUnit(NestedSet): nsm_parent_field = 'parent_healthcare_service_unit' + def validate(self): + self.set_service_unit_properties() + def autoname(self): if self.company: - suffix = " - " + frappe.get_cached_value('Company', self.company, "abbr") + suffix = " - " + frappe.get_cached_value('Company', self.company, 'abbr') if not self.healthcare_service_unit_name.endswith(suffix): self.name = self.healthcare_service_unit_name + suffix else: @@ -22,16 +29,86 @@ class HealthcareServiceUnit(NestedSet): super(HealthcareServiceUnit, self).on_update() self.validate_one_root() - def after_insert(self): - if self.is_group: - self.allow_appointments = 0 - self.overlap_appointments = 0 - self.inpatient_occupancy = 0 - elif self.service_unit_type: + def set_service_unit_properties(self): + if cint(self.is_group): + self.allow_appointments = False + self.overlap_appointments = False + self.inpatient_occupancy = False + self.service_unit_capacity = 0 + self.occupancy_status = '' + self.service_unit_type = '' + elif self.service_unit_type != '': service_unit_type = frappe.get_doc('Healthcare Service Unit Type', self.service_unit_type) self.allow_appointments = service_unit_type.allow_appointments - self.overlap_appointments = service_unit_type.overlap_appointments self.inpatient_occupancy = service_unit_type.inpatient_occupancy - if self.inpatient_occupancy: + + if self.inpatient_occupancy and self.occupancy_status != '': self.occupancy_status = 'Vacant' - self.overlap_appointments = 0 + + if service_unit_type.overlap_appointments: + self.overlap_appointments = True + else: + self.overlap_appointments = False + self.service_unit_capacity = 0 + + if self.overlap_appointments: + if not self.service_unit_capacity: + frappe.throw(_('Please set a valid Service Unit Capacity to enable Overlapping Appointments'), + title=_('Mandatory')) + + +@frappe.whitelist() +def add_multiple_service_units(parent, data): + ''' + parent - parent service unit under which the service units are to be created + data (dict) - company, healthcare_service_unit_name, count, service_unit_type, warehouse, service_unit_capacity + ''' + if not parent or not data: + return + + data = json.loads(data) + company = data.get('company') or \ + frappe.defaults.get_defaults().get('company') or \ + frappe.db.get_single_value('Global Defaults', 'default_company') + + if not data.get('healthcare_service_unit_name') or not company: + frappe.throw(_('Service Unit Name and Company are mandatory to create Healthcare Service Units'), + title=_('Missing Required Fields')) + + count = cint(data.get('count') or 0) + if count <= 0: + frappe.throw(_('Number of Service Units to be created should at least be 1'), + title=_('Invalid Number of Service Units')) + + capacity = cint(data.get('service_unit_capacity') or 1) + + service_unit = { + 'doctype': 'Healthcare Service Unit', + 'parent_healthcare_service_unit': parent, + 'service_unit_type': data.get('service_unit_type') or None, + 'service_unit_capacity': capacity if capacity > 0 else 1, + 'warehouse': data.get('warehouse') or None, + 'company': company + } + + service_unit_name = '{}'.format(data.get('healthcare_service_unit_name').strip(' -')) + + last_suffix = frappe.db.sql("""SELECT + IFNULL(MAX(CAST(SUBSTRING(name FROM %(start)s FOR 4) AS UNSIGNED)), 0) + FROM `tabHealthcare Service Unit` + WHERE name like %(prefix)s AND company=%(company)s""", + {'start': len(service_unit_name)+2, 'prefix': '{}-%'.format(service_unit_name), 'company': company}, + as_list=1)[0][0] + start_suffix = cint(last_suffix) + 1 + + failed_list = [] + for i in range(start_suffix, count + start_suffix): + # name to be in the form WARD-#### + service_unit['healthcare_service_unit_name'] = '{}-{}'.format(service_unit_name, cstr('%0*d' % (4, i))) + service_unit_doc = frappe.get_doc(service_unit) + try: + service_unit_doc.insert() + except Exception: + failed_list.append(service_unit['healthcare_service_unit_name']) + + return failed_list diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js index b75f271827..ea3fea6b7a 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js @@ -1,35 +1,185 @@ -frappe.treeview_settings["Healthcare Service Unit"] = { - breadcrumbs: "Healthcare Service Unit", - title: __("Healthcare Service Unit"), +frappe.provide("frappe.treeview_settings"); + +frappe.treeview_settings['Healthcare Service Unit'] = { + breadcrumbs: 'Healthcare Service Unit', + title: __('Service Unit Tree'), get_tree_root: false, - filters: [{ - fieldname: "company", - fieldtype: "Select", - options: erpnext.utils.get_tree_options("company"), - label: __("Company"), - default: erpnext.utils.get_tree_default("company") - }], get_tree_nodes: 'erpnext.healthcare.utils.get_children', - ignore_fields:["parent_healthcare_service_unit"], - onrender: function(node) { - if (node.data.occupied_out_of_vacant!==undefined) { - $('' - + " " + node.data.occupied_out_of_vacant + filters: [{ + fieldname: 'company', + fieldtype: 'Select', + options: erpnext.utils.get_tree_options('company'), + label: __('Company'), + default: erpnext.utils.get_tree_default('company') + }], + fields: [ + { + fieldtype: 'Data', fieldname: 'healthcare_service_unit_name', label: __('New Service Unit Name'), + reqd: true + }, + { + fieldtype: 'Check', fieldname: 'is_group', label: __('Is Group'), + description: __("Child nodes can be only created under 'Group' type nodes") + }, + { + fieldtype: 'Link', fieldname: 'service_unit_type', label: __('Service Unit Type'), + options: 'Healthcare Service Unit Type', description: __('Type of the new Service Unit'), + depends_on: 'eval:!doc.is_group', default: '', + onchange: () => { + if (cur_dialog) { + if (cur_dialog.fields_dict.service_unit_type.value) { + frappe.db.get_value('Healthcare Service Unit Type', + cur_dialog.fields_dict.service_unit_type.value, 'overlap_appointments') + .then(r => { + if (r.message.overlap_appointments) { + cur_dialog.set_df_property('service_unit_capacity', 'hidden', false); + cur_dialog.set_df_property('service_unit_capacity', 'reqd', true); + } else { + cur_dialog.set_df_property('service_unit_capacity', 'hidden', true); + cur_dialog.set_df_property('service_unit_capacity', 'reqd', false); + } + }); + } else { + cur_dialog.set_df_property('service_unit_capacity', 'hidden', true); + cur_dialog.set_df_property('service_unit_capacity', 'reqd', false); + } + } + } + }, + { + fieldtype: 'Int', fieldname: 'service_unit_capacity', label: __('Service Unit Capacity'), + description: __('Sets the number of concurrent appointments allowed'), reqd: false, + depends_on: "eval:!doc.is_group && doc.service_unit_type != ''", hidden: true + }, + { + fieldtype: 'Link', fieldname: 'warehouse', label: __('Warehouse'), options: 'Warehouse', + description: __('Optional, if you want to manage stock separately for this Service Unit'), + depends_on: 'eval:!doc.is_group' + }, + { + fieldtype: 'Link', fieldname: 'company', label: __('Company'), options: 'Company', reqd: true, + default: () => { + return cur_page.page.page.fields_dict.company.value; + } + } + ], + ignore_fields: ['parent_healthcare_service_unit'], + onrender: function (node) { + if (node.data.occupied_of_available !== undefined) { + $("" + + ' ' + node.data.occupied_of_available + '').insertBefore(node.$ul); } - if (node.data && node.data.inpatient_occupancy!==undefined) { + if (node.data && node.data.inpatient_occupancy !== undefined) { if (node.data.inpatient_occupancy == 1) { - if (node.data.occupancy_status == "Occupied") { - $('' - + " " + node.data.occupancy_status + if (node.data.occupancy_status == 'Occupied') { + $("" + + ' ' + node.data.occupancy_status + '').insertBefore(node.$ul); } - if (node.data.occupancy_status == "Vacant") { - $('' - + " " + node.data.occupancy_status + if (node.data.occupancy_status == 'Vacant') { + $("" + + ' ' + node.data.occupancy_status + '').insertBefore(node.$ul); } } } }, + post_render: function (treeview) { + frappe.treeview_settings['Healthcare Service Unit'].treeview = {}; + $.extend(frappe.treeview_settings['Healthcare Service Unit'].treeview, treeview); + }, + toolbar: [ + { + label: __('Add Multiple'), + condition: function (node) { + return node.expandable; + }, + click: function (node) { + const dialog = new frappe.ui.Dialog({ + title: __('Add Multiple Service Units'), + fields: [ + { + fieldtype: 'Data', fieldname: 'healthcare_service_unit_name', label: __('Service Unit Name'), + reqd: true, description: __("Will be serially suffixed to maintain uniquness. Example: 'Ward' will be named as 'Ward-####'"), + }, + { + fieldtype: 'Int', fieldname: 'count', label: __('Number of Service Units'), + reqd: true + }, + { + fieldtype: 'Link', fieldname: 'service_unit_type', label: __('Service Unit Type'), + options: 'Healthcare Service Unit Type', description: __('Type of the new Service Unit'), + depends_on: 'eval:!doc.is_group', default: '', reqd: true, + onchange: () => { + if (cur_dialog) { + if (cur_dialog.fields_dict.service_unit_type.value) { + frappe.db.get_value('Healthcare Service Unit Type', + cur_dialog.fields_dict.service_unit_type.value, 'overlap_appointments') + .then(r => { + if (r.message.overlap_appointments) { + cur_dialog.set_df_property('service_unit_capacity', 'hidden', false); + cur_dialog.set_df_property('service_unit_capacity', 'reqd', true); + } else { + cur_dialog.set_df_property('service_unit_capacity', 'hidden', true); + cur_dialog.set_df_property('service_unit_capacity', 'reqd', false); + } + }); + } else { + cur_dialog.set_df_property('service_unit_capacity', 'hidden', true); + cur_dialog.set_df_property('service_unit_capacity', 'reqd', false); + } + } + } + }, + { + fieldtype: 'Int', fieldname: 'service_unit_capacity', label: __('Service Unit Capacity'), + description: __('Sets the number of concurrent appointments allowed'), reqd: false, + depends_on: "eval:!doc.is_group && doc.service_unit_type != ''", hidden: true + }, + { + fieldtype: 'Link', fieldname: 'warehouse', label: __('Warehouse'), options: 'Warehouse', + description: __('Optional, if you want to manage stock separately for this Service Unit'), + }, + { + fieldtype: 'Link', fieldname: 'company', label: __('Company'), options: 'Company', reqd: true, + default: () => { + return cur_page.page.page.fields_dict.company.get_value(); + } + } + ], + primary_action: () => { + dialog.hide(); + let vals = dialog.get_values(); + if (!vals) return; + + return frappe.call({ + method: 'erpnext.healthcare.doctype.healthcare_service_unit.healthcare_service_unit.add_multiple_service_units', + args: { + parent: node.data.value, + data: vals + }, + callback: function (r) { + if (!r.exc && r.message) { + frappe.treeview_settings['Healthcare Service Unit'].treeview.tree.load_children(node, true); + + frappe.show_alert({ + message: __('{0} Service Units created', [vals.count - r.message.length]), + indicator: 'green' + }); + } else { + frappe.msgprint(__('Could not create Service Units')); + } + }, + freeze: true, + freeze_message: __('Creating {0} Service Units', [vals.count]) + }); + }, + primary_action_label: __('Create') + }); + dialog.show(); + } + } + ], + extend_toolbar: true }; diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/test_healthcare_service_unit.js b/erpnext/healthcare/doctype/healthcare_service_unit/test_healthcare_service_unit.js deleted file mode 100644 index a67a411707..0000000000 --- a/erpnext/healthcare/doctype/healthcare_service_unit/test_healthcare_service_unit.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Healthcare Service Unit", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Healthcare Service Unit - () => frappe.tests.make('Healthcare Service Unit', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.js b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.js index eb33ab68c0..ecf4aa1a4b 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.js +++ b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.js @@ -68,8 +68,8 @@ let change_item_code = function(frm, doc) { if (values) { frappe.call({ "method": "erpnext.healthcare.doctype.healthcare_service_unit_type.healthcare_service_unit_type.change_item_code", - "args": {item: doc.item, item_code: values['item_code'], doc_name: doc.name}, - callback: function () { + "args": { item: doc.item, item_code: values['item_code'], doc_name: doc.name }, + callback: function() { frm.reload_doc(); } }); diff --git a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json index 4b8503d028..9c81c65f6b 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json +++ b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json @@ -29,6 +29,8 @@ { "fieldname": "service_unit_type", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Service Unit Type", "no_copy": 1, @@ -41,6 +43,8 @@ "depends_on": "eval:doc.inpatient_occupancy != 1", "fieldname": "allow_appointments", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Allow Appointments" }, { @@ -49,6 +53,8 @@ "depends_on": "eval:doc.allow_appointments == 1 && doc.inpatient_occupany != 1", "fieldname": "overlap_appointments", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Allow Overlap" }, { @@ -57,6 +63,8 @@ "depends_on": "eval:doc.allow_appointments != 1", "fieldname": "inpatient_occupancy", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Inpatient Occupancy" }, { @@ -65,17 +73,23 @@ "depends_on": "eval:doc.inpatient_occupancy == 1 && doc.allow_appointments != 1", "fieldname": "is_billable", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Is Billable" }, { "depends_on": "is_billable", "fieldname": "item_details", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Item Details" }, { "fieldname": "item", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Item", "no_copy": 1, "options": "Item", @@ -84,6 +98,8 @@ { "fieldname": "item_code", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Item Code", "mandatory_depends_on": "eval: doc.is_billable == 1", "no_copy": 1 @@ -91,6 +107,8 @@ { "fieldname": "item_group", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Item Group", "mandatory_depends_on": "eval: doc.is_billable == 1", "options": "Item Group" @@ -98,6 +116,8 @@ { "fieldname": "uom", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "UOM", "mandatory_depends_on": "eval: doc.is_billable == 1", "options": "UOM" @@ -105,28 +125,38 @@ { "fieldname": "no_of_hours", "fieldtype": "Int", + "hide_days": 1, + "hide_seconds": 1, "label": "UOM Conversion in Hours", "mandatory_depends_on": "eval: doc.is_billable == 1" }, { "fieldname": "column_break_11", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "rate", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Rate / UOM" }, { "default": "0", "fieldname": "disabled", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Disabled", "no_copy": 1 }, { "fieldname": "description", "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Description" }, { @@ -134,11 +164,13 @@ "fieldname": "change_in_item", "fieldtype": "Check", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Change in Item" } ], "links": [], - "modified": "2020-05-20 15:31:09.627516", + "modified": "2021-08-19 17:52:30.266667", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Service Unit Type", diff --git a/erpnext/healthcare/doctype/healthcare_service_unit_type/test_healthcare_service_unit_type.js b/erpnext/healthcare/doctype/healthcare_service_unit_type/test_healthcare_service_unit_type.js deleted file mode 100644 index 6db8f9e9c1..0000000000 --- a/erpnext/healthcare/doctype/healthcare_service_unit_type/test_healthcare_service_unit_type.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Healthcare Service Unit Type", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Healthcare Service Unit Type - () => frappe.tests.make('Healthcare Service Unit Type', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/healthcare_settings/test_healthcare_settings.js b/erpnext/healthcare/doctype/healthcare_settings/test_healthcare_settings.js deleted file mode 100644 index ca10925e59..0000000000 --- a/erpnext/healthcare/doctype/healthcare_settings/test_healthcare_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Healthcare Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Healthcare Settings - () => frappe.tests.make('Healthcare Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.js b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.js deleted file mode 100644 index 1ce9afa96d..0000000000 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Inpatient Record", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Inpatient Record - () => frappe.tests.make('Inpatient Record', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index a8c7720a0a..b4a961264f 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -151,7 +151,7 @@ def get_healthcare_service_unit(unit_name=None): if not service_unit: service_unit = frappe.new_doc("Healthcare Service Unit") - service_unit.healthcare_service_unit_name = unit_name or "Test Service Unit Ip Occupancy" + service_unit.healthcare_service_unit_name = unit_name or "_Test Service Unit Ip Occupancy" service_unit.company = "_Test Company" service_unit.service_unit_type = get_service_unit_type() service_unit.inpatient_occupancy = 1 @@ -159,12 +159,12 @@ def get_healthcare_service_unit(unit_name=None): service_unit.is_group = 0 service_unit_parent_name = frappe.db.exists({ "doctype": "Healthcare Service Unit", - "healthcare_service_unit_name": "All Healthcare Service Units", + "healthcare_service_unit_name": "_Test All Healthcare Service Units", "is_group": 1 }) if not service_unit_parent_name: parent_service_unit = frappe.new_doc("Healthcare Service Unit") - parent_service_unit.healthcare_service_unit_name = "All Healthcare Service Units" + parent_service_unit.healthcare_service_unit_name = "_Test All Healthcare Service Units" parent_service_unit.is_group = 1 parent_service_unit.save(ignore_permissions = True) service_unit.parent_healthcare_service_unit = parent_service_unit.name @@ -180,7 +180,7 @@ def get_service_unit_type(): if not service_unit_type: service_unit_type = frappe.new_doc("Healthcare Service Unit Type") - service_unit_type.service_unit_type = "Test Service Unit Type Ip Occupancy" + service_unit_type.service_unit_type = "_Test Service Unit Type Ip Occupancy" service_unit_type.inpatient_occupancy = 1 service_unit_type.save(ignore_permissions = True) return service_unit_type.name diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 4b57cd073d..74495a8591 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -34,7 +34,7 @@ class LabTest(Document): frappe.db.set_value('Lab Prescription', self.prescription, 'lab_test_created', 1) if frappe.db.get_value('Lab Prescription', self.prescription, 'invoiced'): self.invoiced = True - if not self.lab_test_name and self.template: + if self.template: self.load_test_from_template() self.reload() @@ -50,7 +50,7 @@ class LabTest(Document): item.secondary_uom_result = float(item.result_value) * float(item.conversion_factor) except: item.secondary_uom_result = '' - frappe.msgprint(_('Row #{0}: Result for Secondary UOM not calculated'.format(item.idx)), title = _('Warning')) + frappe.msgprint(_('Row #{0}: Result for Secondary UOM not calculated').format(item.idx), title = _('Warning')) def validate_result_values(self): if self.normal_test_items: @@ -229,9 +229,9 @@ def create_sample_doc(template, patient, invoice, company = None): sample_collection = frappe.get_doc('Sample Collection', sample_exists[0][0]) quantity = int(sample_collection.sample_qty) + int(template.sample_qty) if template.sample_details: - sample_details = sample_collection.sample_details + '\n-\n' + _('Test: ') + sample_details = sample_collection.sample_details + '\n-\n' + _('Test :') sample_details += (template.get('lab_test_name') or template.get('template')) + '\n' - sample_details += _('Collection Details: ') + '\n\t' + template.sample_details + sample_details += _('Collection Details:') + '\n\t' + template.sample_details frappe.db.set_value('Sample Collection', sample_collection.name, 'sample_details', sample_details) frappe.db.set_value('Sample Collection', sample_collection.name, 'sample_qty', quantity) diff --git a/erpnext/healthcare/doctype/lab_test/test_lab_test.js b/erpnext/healthcare/doctype/lab_test/test_lab_test.js deleted file mode 100644 index 57cb22b269..0000000000 --- a/erpnext/healthcare/doctype/lab_test/test_lab_test.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Lab Test", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Lab Test - () => frappe.tests.make('Lab Test', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/lab_test_sample/test_lab_test_sample.js b/erpnext/healthcare/doctype/lab_test_sample/test_lab_test_sample.js deleted file mode 100644 index ace60de752..0000000000 --- a/erpnext/healthcare/doctype/lab_test_sample/test_lab_test_sample.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Lab Test Sample", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Lab Test Sample - () => frappe.tests.make('Lab Test Sample', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/lab_test_template/test_lab_test_template.js b/erpnext/healthcare/doctype/lab_test_template/test_lab_test_template.js deleted file mode 100644 index 7c2ec8c348..0000000000 --- a/erpnext/healthcare/doctype/lab_test_template/test_lab_test_template.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Lab Test Template", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Lab Test Template - () => frappe.tests.make('Lab Test Template', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/lab_test_uom/test_lab_test_uom.js b/erpnext/healthcare/doctype/lab_test_uom/test_lab_test_uom.js deleted file mode 100644 index 1328dda282..0000000000 --- a/erpnext/healthcare/doctype/lab_test_uom/test_lab_test_uom.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Lab Test UOM", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Lab Test UOM - () => frappe.tests.make('Lab Test UOM', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/medical_code/test_medical_code.js b/erpnext/healthcare/doctype/medical_code/test_medical_code.js deleted file mode 100644 index 8cc7c40025..0000000000 --- a/erpnext/healthcare/doctype/medical_code/test_medical_code.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Medical Code", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Medical Code - () => frappe.tests.make('Medical Code', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/medical_code_standard/test_medical_code_standard.js b/erpnext/healthcare/doctype/medical_code_standard/test_medical_code_standard.js deleted file mode 100644 index 6ab6d531df..0000000000 --- a/erpnext/healthcare/doctype/medical_code_standard/test_medical_code_standard.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Medical Code Standard", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Medical Code Standard - () => frappe.tests.make('Medical Code Standard', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/medical_department/test_medical_department.js b/erpnext/healthcare/doctype/medical_department/test_medical_department.js deleted file mode 100644 index fdf49718dc..0000000000 --- a/erpnext/healthcare/doctype/medical_department/test_medical_department.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Medical Department", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Medical Department - () => frappe.tests.make('Medical Department', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/organism/test_organism.js b/erpnext/healthcare/doctype/organism/test_organism.js deleted file mode 100644 index d57e5536c6..0000000000 --- a/erpnext/healthcare/doctype/organism/test_organism.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Organism", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Organism - () => frappe.tests.make('Organism', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/patient/patient.js b/erpnext/healthcare/doctype/patient/patient.js index bce42e51d0..9266467155 100644 --- a/erpnext/healthcare/doctype/patient/patient.js +++ b/erpnext/healthcare/doctype/patient/patient.js @@ -26,31 +26,39 @@ frappe.ui.form.on('Patient', { } if (frm.doc.patient_name && frappe.user.has_role('Physician')) { + frm.add_custom_button(__('Patient Progress'), function() { + frappe.route_options = {'patient': frm.doc.name}; + frappe.set_route('patient-progress'); + }, __('View')); + frm.add_custom_button(__('Patient History'), function() { frappe.route_options = {'patient': frm.doc.name}; frappe.set_route('patient_history'); - },'View'); + }, __('View')); } - if (!frm.doc.__islocal && (frappe.user.has_role('Nursing User') || frappe.user.has_role('Physician'))) { - frm.add_custom_button(__('Vital Signs'), function () { - create_vital_signs(frm); - }, 'Create'); - frm.add_custom_button(__('Medical Record'), function () { - create_medical_record(frm); - }, 'Create'); - frm.add_custom_button(__('Patient Encounter'), function () { - create_encounter(frm); - }, 'Create'); - frm.toggle_enable(['customer'], 0); // ToDo, allow change only if no transactions booked or better, add merge option + frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Patient'}; + frm.toggle_display(['address_html', 'contact_html'], !frm.is_new()); + + if (!frm.is_new()) { + if ((frappe.user.has_role('Nursing User') || frappe.user.has_role('Physician'))) { + frm.add_custom_button(__('Medical Record'), function () { + create_medical_record(frm); + }, 'Create'); + frm.toggle_enable(['customer'], 0); + } + frappe.contacts.render_address_and_contact(frm); + erpnext.utils.set_party_dashboard_indicators(frm); + } else { + frappe.contacts.clear_address_and_contact(frm); } }, + onload: function (frm) { - if (!frm.doc.dob) { - $(frm.fields_dict['age_html'].wrapper).html(''); - } if (frm.doc.dob) { $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(frm.doc.dob)}`); + } else { + $(frm.fields_dict['age_html'].wrapper).html(''); } } }); @@ -59,16 +67,14 @@ frappe.ui.form.on('Patient', 'dob', function(frm) { if (frm.doc.dob) { let today = new Date(); let birthDate = new Date(frm.doc.dob); - if (today < birthDate){ + if (today < birthDate) { frappe.msgprint(__('Please select a valid Date')); frappe.model.set_value(frm.doctype,frm.docname, 'dob', ''); - } - else { + } else { let age_str = get_age(frm.doc.dob); $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${age_str}`); } - } - else { + } else { $(frm.fields_dict['age_html'].wrapper).html(''); } }); diff --git a/erpnext/healthcare/doctype/patient/patient.json b/erpnext/healthcare/doctype/patient/patient.json index 8af1a9ccd7..4092a6a768 100644 --- a/erpnext/healthcare/doctype/patient/patient.json +++ b/erpnext/healthcare/doctype/patient/patient.json @@ -1,6 +1,6 @@ { "actions": [], - "allow_copy": 1, + "allow_events_in_timeline": 1, "allow_import": 1, "allow_rename": 1, "autoname": "naming_series:", @@ -24,12 +24,19 @@ "image", "column_break_14", "status", + "uid", "inpatient_record", "inpatient_status", "report_preference", "mobile", - "email", "phone", + "email", + "invite_user", + "user_id", + "address_contacts", + "address_html", + "column_break_22", + "contact_html", "customer_details_section", "customer", "customer_group", @@ -74,6 +81,7 @@ "fieldtype": "Select", "in_preview": 1, "label": "Inpatient Status", + "no_copy": 1, "options": "\nAdmission Scheduled\nAdmitted\nDischarge Scheduled", "read_only": 1 }, @@ -81,6 +89,7 @@ "fieldname": "inpatient_record", "fieldtype": "Link", "label": "Inpatient Record", + "no_copy": 1, "options": "Inpatient Record", "read_only": 1 }, @@ -101,6 +110,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Full Name", + "no_copy": 1, "read_only": 1, "search_index": 1 }, @@ -118,6 +128,7 @@ "fieldtype": "Select", "in_preview": 1, "label": "Blood Group", + "no_copy": 1, "options": "\nA Positive\nA Negative\nAB Positive\nAB Negative\nB Positive\nB Negative\nO Positive\nO Negative" }, { @@ -125,7 +136,8 @@ "fieldname": "dob", "fieldtype": "Date", "in_preview": 1, - "label": "Date of birth" + "label": "Date of birth", + "no_copy": 1 }, { "fieldname": "age_html", @@ -167,6 +179,7 @@ "fieldtype": "Link", "ignore_user_permissions": 1, "label": "Customer", + "no_copy": 1, "options": "Customer", "set_only_once": 1 }, @@ -183,6 +196,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Mobile", + "no_copy": 1, "options": "Phone" }, { @@ -192,6 +206,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Email", + "no_copy": 1, "options": "Email" }, { @@ -199,6 +214,7 @@ "fieldtype": "Data", "in_filter": 1, "label": "Phone", + "no_copy": 1, "options": "Phone" }, { @@ -230,7 +246,8 @@ "fieldname": "medication", "fieldtype": "Small Text", "ignore_xss_filter": 1, - "label": "Medication" + "label": "Medication", + "no_copy": 1 }, { "fieldname": "column_break_20", @@ -240,13 +257,15 @@ "fieldname": "medical_history", "fieldtype": "Small Text", "ignore_xss_filter": 1, - "label": "Medical History" + "label": "Medical History", + "no_copy": 1 }, { "fieldname": "surgical_history", "fieldtype": "Small Text", "ignore_xss_filter": 1, - "label": "Surgical History" + "label": "Surgical History", + "no_copy": 1 }, { "collapsible": 1, @@ -258,8 +277,8 @@ "fieldname": "occupation", "fieldtype": "Data", "ignore_xss_filter": 1, - "in_standard_filter": 1, - "label": "Occupation" + "label": "Occupation", + "no_copy": 1 }, { "fieldname": "column_break_25", @@ -269,6 +288,7 @@ "fieldname": "marital_status", "fieldtype": "Select", "label": "Marital Status", + "no_copy": 1, "options": "\nSingle\nMarried\nDivorced\nWidow" }, { @@ -281,25 +301,29 @@ "fieldname": "tobacco_past_use", "fieldtype": "Data", "ignore_xss_filter": 1, - "label": "Tobacco Consumption (Past)" + "label": "Tobacco Consumption (Past)", + "no_copy": 1 }, { "fieldname": "tobacco_current_use", "fieldtype": "Data", "ignore_xss_filter": 1, - "label": "Tobacco Consumption (Present)" + "label": "Tobacco Consumption (Present)", + "no_copy": 1 }, { "fieldname": "alcohol_past_use", "fieldtype": "Data", "ignore_xss_filter": 1, - "label": "Alcohol Consumption (Past)" + "label": "Alcohol Consumption (Past)", + "no_copy": 1 }, { "fieldname": "alcohol_current_use", "fieldtype": "Data", "ignore_user_permissions": 1, - "label": "Alcohol Consumption (Present)" + "label": "Alcohol Consumption (Present)", + "no_copy": 1 }, { "fieldname": "column_break_32", @@ -309,13 +333,15 @@ "fieldname": "surrounding_factors", "fieldtype": "Small Text", "ignore_xss_filter": 1, - "label": "Occupational Hazards and Environmental Factors" + "label": "Occupational Hazards and Environmental Factors", + "no_copy": 1 }, { "fieldname": "other_risk_factors", "fieldtype": "Small Text", "ignore_xss_filter": 1, - "label": "Other Risk Factors" + "label": "Other Risk Factors", + "no_copy": 1 }, { "collapsible": 1, @@ -331,7 +357,8 @@ "fieldname": "patient_details", "fieldtype": "Text", "ignore_xss_filter": 1, - "label": "Patient Details" + "label": "Patient Details", + "no_copy": 1 }, { "fieldname": "default_currency", @@ -342,19 +369,22 @@ { "fieldname": "last_name", "fieldtype": "Data", - "label": "Last Name" + "label": "Last Name", + "no_copy": 1 }, { "fieldname": "first_name", "fieldtype": "Data", "label": "First Name", + "no_copy": 1, "oldfieldtype": "Data", "reqd": 1 }, { "fieldname": "middle_name", "fieldtype": "Data", - "label": "Middle Name (optional)" + "label": "Middle Name (optional)", + "no_copy": 1 }, { "collapsible": 1, @@ -389,13 +419,63 @@ "fieldtype": "Link", "label": "Print Language", "options": "Language" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "address_contacts", + "fieldtype": "Section Break", + "label": "Address and Contact", + "options": "fa fa-map-marker" + }, + { + "fieldname": "address_html", + "fieldtype": "HTML", + "label": "Address HTML", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_22", + "fieldtype": "Column Break" + }, + { + "fieldname": "contact_html", + "fieldtype": "HTML", + "label": "Contact HTML", + "no_copy": 1, + "read_only": 1 + }, + { + "allow_in_quick_entry": 1, + "default": "1", + "fieldname": "invite_user", + "fieldtype": "Check", + "label": "Invite as User", + "no_copy": 1, + "read_only_depends_on": "eval: doc.user_id" + }, + { + "fieldname": "user_id", + "fieldtype": "Read Only", + "label": "User ID", + "no_copy": 1, + "options": "User" + }, + { + "allow_in_quick_entry": 1, + "bold": 1, + "fieldname": "uid", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Identification Number (UID)", + "unique": 1 } ], "icon": "fa fa-user", "image_field": "image", "links": [], "max_attachments": 50, - "modified": "2020-04-25 17:24:32.146415", + "modified": "2021-03-14 13:21:09.759906", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient", @@ -453,7 +533,7 @@ ], "quick_entry": 1, "restrict_to_domain": "Healthcare", - "search_fields": "patient_name,mobile,email,phone", + "search_fields": "patient_name,mobile,email,phone,uid", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "ASC", diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py index 56a34007ff..9dae1f68b0 100644 --- a/erpnext/healthcare/doctype/patient/patient.py +++ b/erpnext/healthcare/doctype/patient/patient.py @@ -8,24 +8,27 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import cint, cstr, getdate import dateutil +from frappe.contacts.address_and_contact import load_address_and_contact +from frappe.contacts.doctype.contact.contact import get_default_contact from frappe.model.naming import set_name_by_naming_series from frappe.utils.nestedset import get_root_of from erpnext import get_default_currency from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account, send_registration_sms +from erpnext.accounts.party import get_dashboard_info class Patient(Document): + def onload(self): + '''Load address and contacts in `__onload`''' + load_address_and_contact(self) + self.load_dashboard_info() + def validate(self): self.set_full_name() - self.add_as_website_user() def before_insert(self): self.set_missing_customer_details() def after_insert(self): - self.add_as_website_user() - self.reload() - if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient') and not self.customer: - create_customer(self) if frappe.db.get_single_value('Healthcare Settings', 'collect_registration_fee'): frappe.db.set_value('Patient', self.name, 'status', 'Disabled') else: @@ -49,6 +52,16 @@ class Patient(Document): else: create_customer(self) + self.set_contact() # add or update contact + + if not self.user_id and self.email and self.invite_user: + self.create_website_user() + + def load_dashboard_info(self): + if self.customer: + info = get_dashboard_info('Customer', self.customer, None) + self.set_onload('dashboard_info', info) + def set_full_name(self): if self.last_name: self.patient_name = ' '.join(filter(None, [self.first_name, self.last_name])) @@ -71,18 +84,24 @@ class Patient(Document): if not self.language: self.language = frappe.db.get_single_value('System Settings', 'language') - def add_as_website_user(self): - if self.email: - if not frappe.db.exists ('User', self.email): - user = frappe.get_doc({ - 'doctype': 'User', - 'first_name': self.first_name, - 'last_name': self.last_name, - 'email': self.email, - 'user_type': 'Website User' - }) - user.flags.ignore_permissions = True - user.add_roles('Patient') + def create_website_user(self): + if self.email and not frappe.db.exists('User', self.email): + user = frappe.get_doc({ + 'doctype': 'User', + 'first_name': self.first_name, + 'last_name': self.last_name, + 'email': self.email, + 'user_type': 'Website User', + 'gender': self.sex, + 'phone': self.phone, + 'mobile_no': self.mobile, + 'birth_date': self.dob + }) + user.flags.ignore_permissions = True + user.enabled = True + user.send_welcome_email = True + user.add_roles('Patient') + frappe.db.set_value(self.doctype, self.name, 'user_id', user.name) def autoname(self): patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by') @@ -102,12 +121,19 @@ class Patient(Document): return name + @property + def age(self): + if not self.dob: + return + dob = getdate(self.dob) + age = dateutil.relativedelta.relativedelta(getdate(), dob) + return age + def get_age(self): - age_str = '' - if self.dob: - dob = getdate(self.dob) - age = dateutil.relativedelta.relativedelta(getdate(), dob) - age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)") + age = self.age + if not age: + return + age_str = f'{str(age.years)} {_("Years(s)")} {str(age.months)} {_("Month(s)")} {str(age.days)} {_("Day(s)")}' return age_str @frappe.whitelist() @@ -124,6 +150,58 @@ class Patient(Document): return {'invoice': sales_invoice.name} + def set_contact(self): + if frappe.db.exists('Dynamic Link', {'parenttype':'Contact', 'link_doctype':'Patient', 'link_name':self.name}): + old_doc = self.get_doc_before_save() + if old_doc.email != self.email or old_doc.mobile != self.mobile or old_doc.phone != self.phone: + self.update_contact() + else: + self.reload() + if self.email or self.mobile or self.phone: + contact = frappe.get_doc({ + 'doctype': 'Contact', + 'first_name': self.first_name, + 'middle_name': self.middle_name, + 'last_name': self.last_name, + 'gender': self.sex, + 'is_primary_contact': 1 + }) + contact.append('links', dict(link_doctype='Patient', link_name=self.name)) + if self.customer: + contact.append('links', dict(link_doctype='Customer', link_name=self.customer)) + + contact.insert(ignore_permissions=True) + self.update_contact(contact) # update email, mobile and phone + + def update_contact(self, contact=None): + if not contact: + contact_name = get_default_contact(self.doctype, self.name) + if contact_name: + contact = frappe.get_doc('Contact', contact_name) + + if contact: + if self.email and self.email != contact.email_id: + for email in contact.email_ids: + email.is_primary = True if email.email_id == self.email else False + contact.add_email(self.email, is_primary=True) + contact.set_primary_email() + + if self.mobile and self.mobile != contact.mobile_no: + for mobile in contact.phone_nos: + mobile.is_primary_mobile_no = True if mobile.phone == self.mobile else False + contact.add_phone(self.mobile, is_primary_mobile_no=True) + contact.set_primary('mobile_no') + + if self.phone and self.phone != contact.phone: + for phone in contact.phone_nos: + phone.is_primary_phone = True if phone.phone == self.phone else False + contact.add_phone(self.phone, is_primary_phone=True) + contact.set_primary('phone') + + contact.flags.ignore_validate = True # disable hook TODO: safe? + contact.save(ignore_permissions=True) + + def create_customer(doc): customer = frappe.get_doc({ 'doctype': 'Customer', @@ -149,8 +227,8 @@ def make_invoice(patient, company): sales_invoice.debit_to = get_receivable_account(company) item_line = sales_invoice.append('items') - item_line.item_name = 'Registeration Fee' - item_line.description = 'Registeration Fee' + item_line.item_name = 'Registration Fee' + item_line.description = 'Registration Fee' item_line.qty = 1 item_line.uom = uom item_line.conversion_factor = 1 @@ -174,8 +252,11 @@ def get_patient_detail(patient): return details def get_timeline_data(doctype, name): - """Return timeline data from medical records""" - return dict(frappe.db.sql(''' + ''' + Return Patient's timeline data from medical records + Also include the associated Customer timeline data + ''' + patient_timeline_data = dict(frappe.db.sql(''' SELECT unix_timestamp(communication_date), count(*) FROM @@ -184,3 +265,11 @@ def get_timeline_data(doctype, name): patient=%s and `communication_date` > date_sub(curdate(), interval 1 year) GROUP BY communication_date''', name)) + + customer = frappe.db.get_value(doctype, name, 'customer') + if customer: + from erpnext.accounts.party import get_timeline_data + customer_timeline_data = get_timeline_data('Customer', customer) + patient_timeline_data.update(customer_timeline_data) + + return patient_timeline_data diff --git a/erpnext/healthcare/doctype/patient/patient_dashboard.py b/erpnext/healthcare/doctype/patient/patient_dashboard.py index 39603f77a0..7f7cfa8e5b 100644 --- a/erpnext/healthcare/doctype/patient/patient_dashboard.py +++ b/erpnext/healthcare/doctype/patient/patient_dashboard.py @@ -6,22 +6,33 @@ def get_data(): 'heatmap': True, 'heatmap_message': _('This is based on transactions against this Patient. See timeline below for details'), 'fieldname': 'patient', + 'non_standard_fieldnames': { + 'Payment Entry': 'party' + }, 'transactions': [ { - 'label': _('Appointments and Patient Encounters'), - 'items': ['Patient Appointment', 'Patient Encounter'] + 'label': _('Appointments and Encounters'), + 'items': ['Patient Appointment', 'Vital Signs', 'Patient Encounter'] }, { 'label': _('Lab Tests and Vital Signs'), - 'items': ['Lab Test', 'Sample Collection', 'Vital Signs'] + 'items': ['Lab Test', 'Sample Collection'] }, { - 'label': _('Billing'), - 'items': ['Sales Invoice'] + 'label': _('Rehab and Physiotherapy'), + 'items': ['Patient Assessment', 'Therapy Session', 'Therapy Plan'] }, { - 'label': _('Orders'), - 'items': ['Inpatient Medication Order'] + 'label': _('Surgery'), + 'items': ['Clinical Procedure'] + }, + { + 'label': _('Admissions'), + 'items': ['Inpatient Record', 'Inpatient Medication Order'] + }, + { + 'label': _('Billing and Payments'), + 'items': ['Sales Invoice', 'Payment Entry'] } ] } diff --git a/erpnext/healthcare/doctype/patient/test_patient.js b/erpnext/healthcare/doctype/patient/test_patient.js deleted file mode 100644 index e1d9ecbd24..0000000000 --- a/erpnext/healthcare/doctype/patient/test_patient.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Patient", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially('Patient', [ - // insert a new Patient - () => frappe.tests.make([ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index c6e489ec17..49847d5bc8 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -17,9 +17,9 @@ frappe.ui.form.on('Patient Appointment', { }, refresh: function(frm) { - frm.set_query('patient', function () { + frm.set_query('patient', function() { return { - filters: {'status': 'Active'} + filters: { 'status': 'Active' } }; }); @@ -64,7 +64,7 @@ frappe.ui.form.on('Patient Appointment', { } else { frappe.call({ method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd', - args: {'patient': frm.doc.patient}, + args: { 'patient': frm.doc.patient }, callback: function(data) { if (data.message == true) { if (frm.doc.mode_of_payment && frm.doc.paid_amount) { @@ -97,7 +97,7 @@ frappe.ui.form.on('Patient Appointment', { if (frm.doc.patient) { frm.add_custom_button(__('Patient History'), function() { - frappe.route_options = {'patient': frm.doc.patient}; + frappe.route_options = { 'patient': frm.doc.patient }; frappe.set_route('patient_history'); }, __('View')); } @@ -111,14 +111,14 @@ frappe.ui.form.on('Patient Appointment', { }); if (frm.doc.procedure_template) { - frm.add_custom_button(__('Clinical Procedure'), function(){ + frm.add_custom_button(__('Clinical Procedure'), function() { frappe.model.open_mapped_doc({ method: 'erpnext.healthcare.doctype.clinical_procedure.clinical_procedure.make_procedure', frm: frm, }); }, __('Create')); } else if (frm.doc.therapy_type) { - frm.add_custom_button(__('Therapy Session'),function(){ + frm.add_custom_button(__('Therapy Session'), function() { frappe.model.open_mapped_doc({ method: 'erpnext.healthcare.doctype.therapy_session.therapy_session.create_therapy_session', frm: frm, @@ -148,7 +148,7 @@ frappe.ui.form.on('Patient Appointment', { doctype: 'Patient', name: frm.doc.patient }, - callback: function (data) { + callback: function(data) { let age = null; if (data.message.dob) { age = calculate_age(data.message.dob); @@ -165,7 +165,7 @@ frappe.ui.form.on('Patient Appointment', { }, practitioner: function(frm) { - if (frm.doc.practitioner ) { + if (frm.doc.practitioner) { frm.events.set_payment_details(frm); } }, @@ -230,7 +230,7 @@ frappe.ui.form.on('Patient Appointment', { toggle_payment_fields: function(frm) { frappe.call({ method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.check_payment_fields_reqd', - args: {'patient': frm.doc.patient}, + args: { 'patient': frm.doc.patient }, callback: function(data) { if (data.message.fee_validity) { // if fee validity exists and automated appointment invoicing is enabled, @@ -254,7 +254,7 @@ frappe.ui.form.on('Patient Appointment', { frm.toggle_display('paid_amount', data.message ? 1 : 0); frm.toggle_display('billing_item', data.message ? 1 : 0); frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0); - frm.toggle_reqd('paid_amount', data.message ? 1 :0); + frm.toggle_reqd('paid_amount', data.message ? 1 : 0); frm.toggle_reqd('billing_item', data.message ? 1 : 0); } } @@ -265,7 +265,7 @@ frappe.ui.form.on('Patient Appointment', { if (frm.doc.patient) { frappe.call({ method: "erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_prescribed_therapies", - args: {patient: frm.doc.patient}, + args: { patient: frm.doc.patient }, callback: function(r) { if (r.message) { show_therapy_types(frm, r.message); @@ -302,13 +302,13 @@ let check_and_set_availability = function(frm) { let d = new frappe.ui.Dialog({ title: __('Available slots'), fields: [ - { fieldtype: 'Link', options: 'Medical Department', reqd: 1, fieldname: 'department', label: 'Medical Department'}, - { fieldtype: 'Column Break'}, - { fieldtype: 'Link', options: 'Healthcare Practitioner', reqd: 1, fieldname: 'practitioner', label: 'Healthcare Practitioner'}, - { fieldtype: 'Column Break'}, - { fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date'}, - { fieldtype: 'Section Break'}, - { fieldtype: 'HTML', fieldname: 'available_slots'} + { fieldtype: 'Link', options: 'Medical Department', reqd: 1, fieldname: 'department', label: 'Medical Department' }, + { fieldtype: 'Column Break' }, + { fieldtype: 'Link', options: 'Healthcare Practitioner', reqd: 1, fieldname: 'practitioner', label: 'Healthcare Practitioner' }, + { fieldtype: 'Column Break' }, + { fieldtype: 'Date', reqd: 1, fieldname: 'appointment_date', label: 'Date' }, + { fieldtype: 'Section Break' }, + { fieldtype: 'HTML', fieldname: 'available_slots' } ], primary_action_label: __('Book'), @@ -386,59 +386,22 @@ let check_and_set_availability = function(frm) { let $wrapper = d.fields_dict.available_slots.$wrapper; // make buttons for each slot - let slot_details = data.slot_details; - let slot_html = ''; - for (let i = 0; i < slot_details.length; i++) { - slot_html = slot_html + ``; - slot_html = slot_html + `
` + slot_details[i].avail_slot.map(slot => { - let disabled = ''; - let start_str = slot.from_time; - let slot_start_time = moment(slot.from_time, 'HH:mm:ss'); - let slot_to_time = moment(slot.to_time, 'HH:mm:ss'); - let interval = (slot_to_time - slot_start_time)/60000 | 0; - // iterate in all booked appointments, update the start time and duration - slot_details[i].appointments.forEach(function(booked) { - let booked_moment = moment(booked.appointment_time, 'HH:mm:ss'); - let end_time = booked_moment.clone().add(booked.duration, 'minutes'); - // Deal with 0 duration appointments - if (booked_moment.isSame(slot_start_time) || booked_moment.isBetween(slot_start_time, slot_to_time)) { - if(booked.duration == 0){ - disabled = 'disabled="disabled"'; - return false; - } - } - // Check for overlaps considering appointment duration - if (slot_start_time.isBefore(end_time) && slot_to_time.isAfter(booked_moment)) { - // There is an overlap - disabled = 'disabled="disabled"'; - return false; - } - }); - return ``; - }).join(""); - slot_html = slot_html + `
`; - } + let slot_html = get_slots(data.slot_details); $wrapper .css('margin-bottom', 0) .addClass('text-center') .html(slot_html); - // blue button when clicked + // highlight button when clicked $wrapper.on('click', 'button', function() { let $btn = $(this); - $wrapper.find('button').removeClass('btn-primary'); - $btn.addClass('btn-primary'); + $wrapper.find('button').removeClass('btn-outline-primary'); + $btn.addClass('btn-outline-primary'); selected_slot = $btn.attr('data-name'); service_unit = $btn.attr('data-service-unit'); duration = $btn.attr('data-duration'); - // enable dialog action + // enable primary action 'Book' d.get_primary_btn().attr('disabled', null); }); @@ -448,19 +411,102 @@ let check_and_set_availability = function(frm) { } }, freeze: true, - freeze_message: __('Fetching records......') + freeze_message: __('Fetching Schedule...') }); } else { fd.available_slots.html(__('Appointment date and Healthcare Practitioner are Mandatory').bold()); } } + + function get_slots(slot_details) { + let slot_html = ''; + let appointment_count = 0; + let disabled = false; + let start_str, slot_start_time, slot_end_time, interval, count, count_class, tool_tip, available_slots; + + slot_details.forEach((slot_info) => { + slot_html += `
+ ${__('Practitioner Schedule:')} ${slot_info.slot_name}
+ ${__('Service Unit:')} ${slot_info.service_unit} `; + + if (slot_info.service_unit_capacity) { + slot_html += `
${__('Maximum Capacity:')} ${slot_info.service_unit_capacity} `; + } + + slot_html += '


'; + + slot_html += slot_info.avail_slot.map(slot => { + appointment_count = 0; + disabled = false; + start_str = slot.from_time; + slot_start_time = moment(slot.from_time, 'HH:mm:ss'); + slot_end_time = moment(slot.to_time, 'HH:mm:ss'); + interval = (slot_end_time - slot_start_time) / 60000 | 0; + + // iterate in all booked appointments, update the start time and duration + slot_info.appointments.forEach((booked) => { + let booked_moment = moment(booked.appointment_time, 'HH:mm:ss'); + let end_time = booked_moment.clone().add(booked.duration, 'minutes'); + + // Deal with 0 duration appointments + if (booked_moment.isSame(slot_start_time) || booked_moment.isBetween(slot_start_time, slot_end_time)) { + if (booked.duration == 0) { + disabled = true; + return false; + } + } + + // Check for overlaps considering appointment duration + if (slot_info.allow_overlap != 1) { + if (slot_start_time.isBefore(end_time) && slot_end_time.isAfter(booked_moment)) { + // There is an overlap + disabled = true; + return false; + } + } else { + if (slot_start_time.isBefore(end_time) && slot_end_time.isAfter(booked_moment)) { + appointment_count++; + } + if (appointment_count >= slot_info.service_unit_capacity) { + // There is an overlap + disabled = true; + return false; + } + } + }); + + if (slot_info.allow_overlap == 1 && slot_info.service_unit_capacity > 1) { + available_slots = slot_info.service_unit_capacity - appointment_count; + count = `${(available_slots > 0 ? available_slots : __('Full'))}`; + count_class = `${(available_slots > 0 ? 'badge-success' : 'badge-danger')}`; + tool_tip =`${available_slots} ${__('slots available for booking')}`; + } + return ` + `; + }).join(""); + + if (slot_info.service_unit_capacity) { + slot_html += `
${__('Each slot indicates the capacity currently available for booking')}`; + } + slot_html += `

`; + }); + + return slot_html; + } }; let get_prescribed_procedure = function(frm) { if (frm.doc.patient) { frappe.call({ method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_procedure_prescribed', - args: {patient: frm.doc.patient}, + args: { patient: frm.doc.patient }, callback: function(r) { if (r.message && r.message.length) { show_procedure_templates(frm, r.message); @@ -480,7 +526,7 @@ let get_prescribed_procedure = function(frm) { } }; -let show_procedure_templates = function(frm, result){ +let show_procedure_templates = function(frm, result) { let d = new frappe.ui.Dialog({ title: __('Prescribed Procedures'), fields: [ @@ -500,9 +546,11 @@ let show_procedure_templates = function(frm, result){ data-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\ data-date="%(date)s" data-department="%(department)s">\

', {name:y[0], procedure_template: y[1], - encounter:y[2], consulting_practitioner:y[3], encounter_date:y[4], - practitioner:y[5]? y[5]:'', date: y[6]? y[6]:'', department: y[7]? y[7]:''})).appendTo(html_field); +

', { + name: y[0], procedure_template: y[1], + encounter: y[2], consulting_practitioner: y[3], encounter_date: y[4], + practitioner: y[5] ? y[5] : '', date: y[6] ? y[6] : '', department: y[7] ? y[7] : '' + })).appendTo(html_field); row.find("a").click(function() { frm.doc.procedure_template = $(this).attr('data-procedure-template'); frm.doc.procedure_prescription = $(this).attr('data-name'); @@ -520,7 +568,7 @@ let show_procedure_templates = function(frm, result){ }); if (!result) { let msg = __('There are no procedure prescribed for ') + frm.doc.patient; - $(repl('
%(msg)s
', {msg: msg})).appendTo(html_field); + $(repl('
%(msg)s
', { msg: msg })).appendTo(html_field); } d.show(); }; @@ -535,7 +583,7 @@ let show_therapy_types = function(frm, result) { ] }); var html_field = d.fields_dict.therapy_type.$wrapper; - $.each(result, function(x, y){ + $.each(result, function(x, y) { var row = $(repl('
\
%(encounter)s
%(practitioner)s
%(date)s
\
%(therapy)s
\ @@ -544,9 +592,11 @@ let show_therapy_types = function(frm, result) { data-encounter="%(encounter)s" data-practitioner="%(practitioner)s"\ data-date="%(date)s" data-department="%(department)s">\

', {therapy:y[0], - name: y[1], encounter:y[2], practitioner:y[3], date:y[4], - department:y[6]? y[6]:'', therapy_plan:y[5]})).appendTo(html_field); +

', { + therapy: y[0], + name: y[1], encounter: y[2], practitioner: y[3], date: y[4], + department: y[6] ? y[6] : '', therapy_plan: y[5] + })).appendTo(html_field); row.find("a").click(function() { frm.doc.therapy_type = $(this).attr("data-therapy"); @@ -581,13 +631,13 @@ let create_vital_signs = function(frm) { frappe.new_doc('Vital Signs'); }; -let update_status = function(frm, status){ +let update_status = function(frm, status) { let doc = frm.doc; frappe.confirm(__('Are you sure you want to cancel this appointment?'), function() { frappe.call({ method: 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_status', - args: {appointment_id: doc.name, status:status}, + args: { appointment_id: doc.name, status: status }, callback: function(data) { if (!data.exc) { frm.reload_doc(); diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index 73ec3bc325..28d3a6dadf 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -131,7 +131,7 @@ "fieldtype": "Link", "label": "Service Unit", "options": "Healthcare Service Unit", - "set_only_once": 1 + "read_only": 1 }, { "depends_on": "eval:doc.practitioner;", @@ -349,7 +349,7 @@ } ], "links": [], - "modified": "2021-06-16 00:40:26.841794", + "modified": "2021-08-30 09:00:41.329387", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Appointment", diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 05e2cd30df..f0d5af9341 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document import json -from frappe.utils import getdate, get_time, flt +from frappe.utils import getdate, get_time, flt, get_link_to_form from frappe.model.mapper import get_mapped_doc from frappe import _ import datetime @@ -15,6 +15,11 @@ from erpnext.hr.doctype.employee.employee import is_holiday from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account from erpnext.healthcare.utils import check_fee_validity, get_service_item_and_practitioner_charge, manage_fee_validity +class MaximumCapacityError(frappe.ValidationError): + pass +class OverlapError(frappe.ValidationError): + pass + class PatientAppointment(Document): def validate(self): self.validate_overlaps() @@ -49,26 +54,49 @@ class PatientAppointment(Document): end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \ + datetime.timedelta(minutes=flt(self.duration)) - overlaps = frappe.db.sql(""" - select - name, practitioner, patient, appointment_time, duration - from - `tabPatient Appointment` - where - appointment_date=%s and name!=%s and status NOT IN ("Closed", "Cancelled") - and (practitioner=%s or patient=%s) and - ((appointment_time<%s and appointment_time + INTERVAL duration MINUTE>%s) or - (appointment_time>%s and appointment_time<%s) or - (appointment_time=%s)) - """, (self.appointment_date, self.name, self.practitioner, self.patient, - self.appointment_time, end_time.time(), self.appointment_time, end_time.time(), self.appointment_time)) + # all appointments for both patient and practitioner overlapping the duration of this appointment + overlapping_appointments = frappe.db.sql(""" + SELECT + name, practitioner, patient, appointment_time, duration, service_unit + FROM + `tabPatient Appointment` + WHERE + appointment_date=%(appointment_date)s AND name!=%(name)s AND status NOT IN ("Closed", "Cancelled") AND + (practitioner=%(practitioner)s OR patient=%(patient)s) AND + ((appointment_time<%(appointment_time)s AND appointment_time + INTERVAL duration MINUTE>%(appointment_time)s) OR + (appointment_time>%(appointment_time)s AND appointment_time<%(end_time)s) OR + (appointment_time=%(appointment_time)s)) + """, + { + 'appointment_date': self.appointment_date, + 'name': self.name, + 'practitioner': self.practitioner, + 'patient': self.patient, + 'appointment_time': self.appointment_time, + 'end_time':end_time.time() + }, + as_dict = True + ) + + if not overlapping_appointments: + return # No overlaps, nothing to validate! + + if self.service_unit: # validate service unit capacity if overlap enabled + allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', self.service_unit, + ['overlap_appointments', 'service_unit_capacity']) + if allow_overlap: + service_unit_appointments = list(filter(lambda appointment: appointment['service_unit'] == self.service_unit and + appointment['patient'] != self.patient, overlapping_appointments)) # if same patient already booked, it should be an overlap + if len(service_unit_appointments) >= (service_unit_capacity or 1): + frappe.throw(_("Not allowed, {} cannot exceed maximum capacity {}") + .format(frappe.bold(self.service_unit), frappe.bold(service_unit_capacity or 1)), MaximumCapacityError) + else: # service_unit_appointments within capacity, remove from overlapping_appointments + overlapping_appointments = [appointment for appointment in overlapping_appointments if appointment not in service_unit_appointments] + + if overlapping_appointments: + frappe.throw(_("Not allowed, cannot overlap appointment {}") + .format(frappe.bold(', '.join([appointment['name'] for appointment in overlapping_appointments]))), OverlapError) - if overlaps: - overlapping_details = _('Appointment overlaps with ') - overlapping_details += "{0}
".format(overlaps[0][0]) - overlapping_details += _('{0} has appointment scheduled with {1} at {2} having {3} minute(s) duration.').format( - overlaps[0][1], overlaps[0][2], overlaps[0][3], overlaps[0][4]) - frappe.throw(overlapping_details, title=_('Appointments Overlapping')) def validate_service_unit(self): if self.inpatient_record and self.service_unit: @@ -109,9 +137,13 @@ class PatientAppointment(Document): frappe.db.set_value('Patient Appointment', self.name, 'notes', comments) def update_fee_validity(self): + if not frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups'): + return + fee_validity = manage_fee_validity(self) if fee_validity: - frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till)) + frappe.msgprint(_('{0}: {1} has fee validity till {2}').format(self.patient, + frappe.bold(self.patient_name), fee_validity.valid_till)) @frappe.whitelist() def get_therapy_types(self): @@ -301,17 +333,13 @@ def check_employee_wise_availability(date, practitioner_doc): def get_available_slots(practitioner_doc, date): - available_slots = [] - slot_details = [] + available_slots = slot_details = [] weekday = date.strftime('%A') practitioner = practitioner_doc.name for schedule_entry in practitioner_doc.practitioner_schedules: - if schedule_entry.schedule: - practitioner_schedule = frappe.get_doc('Practitioner Schedule', schedule_entry.schedule) - else: - frappe.throw(_('{0} does not have a Healthcare Practitioner Schedule. Add it in Healthcare Practitioner').format( - frappe.bold(practitioner)), title=_('Practitioner Schedule Not Found')) + validate_practitioner_schedules(schedule_entry, practitioner) + practitioner_schedule = frappe.get_doc('Practitioner Schedule', schedule_entry.schedule) if practitioner_schedule: available_slots = [] @@ -321,6 +349,8 @@ def get_available_slots(practitioner_doc, date): if available_slots: appointments = [] + allow_overlap = 0 + service_unit_capacity = 0 # fetch all appointments to practitioner by service unit filters = { 'practitioner': practitioner, @@ -330,8 +360,8 @@ def get_available_slots(practitioner_doc, date): } if schedule_entry.service_unit: - slot_name = schedule_entry.schedule + ' - ' + schedule_entry.service_unit - allow_overlap = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, 'overlap_appointments') + slot_name = f'{schedule_entry.schedule}' + allow_overlap, service_unit_capacity = frappe.get_value('Healthcare Service Unit', schedule_entry.service_unit, ['overlap_appointments', 'service_unit_capacity']) if not allow_overlap: # fetch all appointments to service unit filters.pop('practitioner') @@ -346,12 +376,25 @@ def get_available_slots(practitioner_doc, date): filters=filters, fields=['name', 'appointment_time', 'duration', 'status']) - slot_details.append({'slot_name':slot_name, 'service_unit':schedule_entry.service_unit, - 'avail_slot':available_slots, 'appointments': appointments}) + slot_details.append({'slot_name': slot_name, 'service_unit': schedule_entry.service_unit, 'avail_slot': available_slots, + 'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity}) return slot_details +def validate_practitioner_schedules(schedule_entry, practitioner): + if schedule_entry.schedule: + if not schedule_entry.service_unit: + frappe.throw(_('Practitioner {0} does not have a Service Unit set against the Practitioner Schedule {1}.').format( + get_link_to_form('Healthcare Practitioner', practitioner), frappe.bold(schedule_entry.schedule)), + title=_('Service Unit Not Found')) + + else: + frappe.throw(_('Practitioner {0} does not have a Practitioner Schedule assigned.').format( + get_link_to_form('Healthcare Practitioner', practitioner)), + title=_('Practitioner Schedule Not Found')) + + @frappe.whitelist() def update_status(appointment_id, status): frappe.db.set_value('Patient Appointment', appointment_id, 'status', status) diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.js deleted file mode 100644 index 71fc177845..0000000000 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Patient Appointment", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Patient Appointment - () => frappe.tests.make('Patient Appointment', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 9c3392cd5b..36ef2d1623 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -16,9 +16,11 @@ class TestPatientAppointment(unittest.TestCase): frappe.db.sql("""delete from `tabFee Validity`""") frappe.db.sql("""delete from `tabPatient Encounter`""") make_pos_profile() + frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test %'""") + frappe.db.sql("""delete from `tabHealthcare Service Unit` where name like '_Test Service Unit Type%'""") def test_status(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0) appointment = create_appointment(patient, practitioner, nowdate()) self.assertEqual(appointment.status, 'Open') @@ -30,7 +32,7 @@ class TestPatientAppointment(unittest.TestCase): self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open') def test_start_encounter(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1) appointment.reload() @@ -44,7 +46,7 @@ class TestPatientAppointment(unittest.TestCase): self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced')) def test_auto_invoicing(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0) appointment = create_appointment(patient, practitioner, nowdate()) @@ -60,13 +62,14 @@ class TestPatientAppointment(unittest.TestCase): self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount) def test_auto_invoicing_based_on_department(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() + medical_department = create_medical_department() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) - appointment_type = create_appointment_type() + appointment_type = create_appointment_type({'medical_department': medical_department}) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), - invoice=1, appointment_type=appointment_type.name, department='_Test Medical Department') + invoice=1, appointment_type=appointment_type.name, department=medical_department) appointment.reload() self.assertEqual(appointment.invoiced, 1) @@ -78,7 +81,7 @@ class TestPatientAppointment(unittest.TestCase): self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount) def test_auto_invoicing_according_to_appointment_type_charge(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) @@ -88,9 +91,9 @@ class TestPatientAppointment(unittest.TestCase): 'op_consulting_charge': 300 }] appointment_type = create_appointment_type(args={ - 'name': 'Generic Appointment Type charge', - 'items': items - }) + 'name': 'Generic Appointment Type charge', + 'items': items + }) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2), invoice=1, appointment_type=appointment_type.name) @@ -104,21 +107,24 @@ class TestPatientAppointment(unittest.TestCase): self.assertTrue(sales_invoice_name) def test_appointment_cancel(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1) appointment = create_appointment(patient, practitioner, nowdate()) - fee_validity = frappe.db.get_value('Fee Validity Reference', {'appointment': appointment.name}, 'parent') + fee_validity = frappe.db.get_value('Fee Validity', {'patient': patient, 'practitioner': practitioner}) # fee validity created self.assertTrue(fee_validity) - visited = frappe.db.get_value('Fee Validity', fee_validity, 'visited') + # first follow up appointment + appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1)) + self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), 1) + update_status(appointment.name, 'Cancelled') # check fee validity updated - self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), visited - 1) + self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), 0) frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) - appointment = create_appointment(patient, practitioner, nowdate(), invoice=1) + appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1) update_status(appointment.name, 'Cancelled') # check invoice cancelled sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') @@ -130,7 +136,7 @@ class TestPatientAppointment(unittest.TestCase): create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy frappe.db.sql("""delete from `tabInpatient Record`""") - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() patient = create_patient() # Schedule Admission ip_record = create_inpatient(patient) @@ -138,7 +144,7 @@ class TestPatientAppointment(unittest.TestCase): ip_record.save(ignore_permissions = True) # Admit - service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy') + service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy') admit_patient(ip_record, service_unit, now_datetime()) appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit) @@ -156,7 +162,7 @@ class TestPatientAppointment(unittest.TestCase): create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy frappe.db.sql("""delete from `tabInpatient Record`""") - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() patient = create_patient() # Schedule Admission ip_record = create_inpatient(patient) @@ -164,10 +170,10 @@ class TestPatientAppointment(unittest.TestCase): ip_record.save(ignore_permissions = True) # Admit - service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy') + service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy') admit_patient(ip_record, service_unit, now_datetime()) - appointment_service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy for Appointment') + appointment_service_unit = get_healthcare_service_unit('_Test Service Unit Ip Occupancy for Appointment') appointment = create_appointment(patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0) self.assertRaises(frappe.exceptions.ValidationError, appointment.save) @@ -189,7 +195,7 @@ class TestPatientAppointment(unittest.TestCase): assert payment_required is True def test_sales_invoice_should_be_generated_for_new_patient_appointment(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) invoice_count = frappe.db.count('Sales Invoice') @@ -200,10 +206,10 @@ class TestPatientAppointment(unittest.TestCase): assert new_invoice_count == invoice_count + 1 def test_patient_appointment_should_consider_permissions_while_fetching_appointments(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() create_appointment(patient, practitioner, nowdate()) - patient, medical_department, new_practitioner = create_healthcare_docs(practitioner_name='Dr. John') + patient, new_practitioner = create_healthcare_docs(id=5) create_appointment(patient, new_practitioner, nowdate()) roles = [{"doctype": "Has Role", "role": "Physician"}] @@ -220,41 +226,102 @@ class TestPatientAppointment(unittest.TestCase): appointments = frappe.get_list('Patient Appointment') assert len(appointments) == 2 -def create_healthcare_docs(practitioner_name=None): - if not practitioner_name: - practitioner_name = '_Test Healthcare Practitioner' + def test_overlap_appointment(self): + from erpnext.healthcare.doctype.patient_appointment.patient_appointment import OverlapError + patient, practitioner = create_healthcare_docs(id=1) + patient_1, practitioner_1 = create_healthcare_docs(id=2) + service_unit = create_service_unit(id=0) + service_unit_1 = create_service_unit(id=1) + appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit) # valid - patient = create_patient() - practitioner = frappe.db.exists('Healthcare Practitioner', practitioner_name) - medical_department = frappe.db.exists('Medical Department', '_Test Medical Department') + # patient and practitioner cannot have overlapping appointments + appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit, save=0) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit_1, save=0) # diff service unit + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient, practitioner, nowdate(), save=0) # with no service unit link + self.assertRaises(OverlapError, appointment.save) - if not medical_department: - medical_department = frappe.new_doc('Medical Department') - medical_department.department = '_Test Medical Department' - medical_department.save(ignore_permissions=True) - medical_department = medical_department.name + # patient cannot have overlapping appointments with other practitioners + appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit, save=0) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient, practitioner_1, nowdate(), service_unit=service_unit_1, save=0) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient, practitioner_1, nowdate(), save=0) + self.assertRaises(OverlapError, appointment.save) - if not practitioner: - practitioner = frappe.new_doc('Healthcare Practitioner') - practitioner.first_name = practitioner_name - practitioner.gender = 'Female' - practitioner.department = medical_department - practitioner.op_consulting_charge = 500 - practitioner.inpatient_visit_charge = 500 - practitioner.save(ignore_permissions=True) - practitioner = practitioner.name + # practitioner cannot have overlapping appointments with other patients + appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit, save=0) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient_1, practitioner, nowdate(), service_unit=service_unit_1, save=0) + self.assertRaises(OverlapError, appointment.save) + appointment = create_appointment(patient_1, practitioner, nowdate(), save=0) + self.assertRaises(OverlapError, appointment.save) - return patient, medical_department, practitioner + def test_service_unit_capacity(self): + from erpnext.healthcare.doctype.patient_appointment.patient_appointment import MaximumCapacityError, OverlapError + practitioner = create_practitioner() + capacity = 3 + overlap_service_unit_type = create_service_unit_type(id=10, allow_appointments=1, overlap_appointments=1) + overlap_service_unit = create_service_unit(id=100, service_unit_type=overlap_service_unit_type, service_unit_capacity=capacity) + + for i in range(0, capacity): + patient = create_patient(id=i) + create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit) # valid + appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0) # overlap + self.assertRaises(OverlapError, appointment.save) + + patient = create_patient(id=capacity) + appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0) + self.assertRaises(MaximumCapacityError, appointment.save) + + +def create_healthcare_docs(id=0): + patient = create_patient(id) + practitioner = create_practitioner(id) + + return patient, practitioner + + +def create_patient(id=0): + if frappe.db.exists('Patient', {'firstname':f'_Test Patient {str(id)}'}): + patient = frappe.db.get_value('Patient', {'first_name': f'_Test Patient {str(id)}'}, ['name']) + return patient + + patient = frappe.new_doc('Patient') + patient.first_name = f'_Test Patient {str(id)}' + patient.sex = 'Female' + patient.save(ignore_permissions=True) + + return patient.name + + +def create_medical_department(id=0): + if frappe.db.exists('Medical Department', f'_Test Medical Department {str(id)}'): + return f'_Test Medical Department {str(id)}' + + medical_department = frappe.new_doc('Medical Department') + medical_department.department = f'_Test Medical Department {str(id)}' + medical_department.save(ignore_permissions=True) + + return medical_department.name + + +def create_practitioner(id=0, medical_department=None): + if frappe.db.exists('Healthcare Practitioner', {'firstname':f'_Test Healthcare Practitioner {str(id)}'}): + practitioner = frappe.db.get_value('Healthcare Practitioner', {'firstname':f'_Test Healthcare Practitioner {str(id)}'}, ['name']) + return practitioner + + practitioner = frappe.new_doc('Healthcare Practitioner') + practitioner.first_name = f'_Test Healthcare Practitioner {str(id)}' + practitioner.gender = 'Female' + practitioner.department = medical_department or create_medical_department(id) + practitioner.op_consulting_charge = 500 + practitioner.inpatient_visit_charge = 500 + practitioner.save(ignore_permissions=True) + + return practitioner.name -def create_patient(): - patient = frappe.db.exists('Patient', '_Test Patient') - if not patient: - patient = frappe.new_doc('Patient') - patient.first_name = '_Test Patient' - patient.sex = 'Female' - patient.save(ignore_permissions=True) - patient = patient.name - return patient def create_encounter(appointment): if appointment: @@ -267,8 +334,10 @@ def create_encounter(appointment): encounter.company = appointment.company encounter.save() encounter.submit() + return encounter + def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, appointment_type=None, save=1, department=None): item = create_healthcare_service_items() @@ -281,6 +350,7 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce appointment.appointment_date = appointment_date appointment.company = '_Test Company' appointment.duration = 15 + if service_unit: appointment.service_unit = service_unit if invoice: @@ -291,11 +361,14 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce appointment.procedure_template = create_clinical_procedure_template().get('name') if save: appointment.save(ignore_permissions=True) + return appointment + def create_healthcare_service_items(): if frappe.db.exists('Item', 'HLC-SI-001'): return 'HLC-SI-001' + item = frappe.new_doc('Item') item.item_code = 'HLC-SI-001' item.item_name = 'Consulting Charges' @@ -303,11 +376,14 @@ def create_healthcare_service_items(): item.is_stock_item = 0 item.stock_uom = 'Nos' item.save() + return item.name + def create_clinical_procedure_template(): if frappe.db.exists('Clinical Procedure Template', 'Knee Surgery and Rehab'): return frappe.get_doc('Clinical Procedure Template', 'Knee Surgery and Rehab') + template = frappe.new_doc('Clinical Procedure Template') template.template = 'Knee Surgery and Rehab' template.item_code = 'Knee Surgery and Rehab' @@ -316,8 +392,10 @@ def create_clinical_procedure_template(): template.description = 'Knee Surgery and Rehab' template.rate = 50000 template.save() + return template + def create_appointment_type(args=None): if not args: args = frappe.local.form_dict @@ -330,9 +408,9 @@ def create_appointment_type(args=None): else: item = create_healthcare_service_items() items = [{ - 'medical_department': '_Test Medical Department', - 'op_consulting_charge_item': item, - 'op_consulting_charge': 200 + 'medical_department': args.get('medical_department') or '_Test Medical Department', + 'op_consulting_charge_item': item, + 'op_consulting_charge': 200 }] return frappe.get_doc({ 'doctype': 'Appointment Type', @@ -356,3 +434,30 @@ def create_user(email=None, roles=None): "roles": roles, }).insert() return user + + +def create_service_unit_type(id=0, allow_appointments=1, overlap_appointments=0): + if frappe.db.exists('Healthcare Service Unit Type', f'_Test Service Unit Type {str(id)}'): + return f'_Test Service Unit Type {str(id)}' + + service_unit_type = frappe.new_doc('Healthcare Service Unit Type') + service_unit_type.service_unit_type = f'_Test Service Unit Type {str(id)}' + service_unit_type.allow_appointments = allow_appointments + service_unit_type.overlap_appointments = overlap_appointments + service_unit_type.save(ignore_permissions=True) + + return service_unit_type.name + + +def create_service_unit(id=0, service_unit_type=None, service_unit_capacity=0): + if frappe.db.exists('Healthcare Service Unit', f'_Test Service Unit {str(id)}'): + return f'_Test service_unit {str(id)}' + + service_unit = frappe.new_doc('Healthcare Service Unit') + service_unit.is_group = 0 + service_unit.healthcare_service_unit_name= f'_Test Service Unit {str(id)}' + service_unit.service_unit_type = service_unit_type or create_service_unit_type(id) + service_unit.service_unit_capacity = service_unit_capacity + service_unit.save(ignore_permissions=True) + + return service_unit.name diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js index aaeaa692e6..c3466260d2 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js @@ -185,7 +185,42 @@ frappe.ui.form.on('Patient Encounter', { }; frm.set_value(values); } - } + }, + + get_applicable_treatment_plans: function(frm) { + frappe.call({ + method: 'get_applicable_treatment_plans', + doc: frm.doc, + args: {'encounter': frm.doc}, + freeze: true, + freeze_message: __('Fetching Treatment Plans'), + callback: function() { + new frappe.ui.form.MultiSelectDialog({ + doctype: "Treatment Plan Template", + target: this.cur_frm, + setters: { + medical_department: "", + }, + action(selections) { + frappe.call({ + method: 'set_treatment_plans', + doc: frm.doc, + args: selections, + }).then(() => { + frm.refresh_field('drug_prescription'); + frm.refresh_field('procedure_prescription'); + frm.refresh_field('lab_test_prescription'); + frm.refresh_field('therapies'); + }); + cur_dialog.hide(); + } + }); + + + } + }); + }, + }); var schedule_inpatient = function(frm) { diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json index b646ff9ebe..994597dca7 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json @@ -31,6 +31,7 @@ "sb_symptoms", "symptoms", "symptoms_in_print", + "get_applicable_treatment_plans", "physical_examination", "diagnosis", "diagnosis_in_print", @@ -324,11 +325,17 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval:doc.patient", + "fieldname": "get_applicable_treatment_plans", + "fieldtype": "Button", + "label": "Get Applicable Treatment Plans" } ], "is_submittable": 1, "links": [], - "modified": "2020-11-30 10:39:00.783119", + "modified": "2021-07-27 11:39:12.347704", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Encounter", @@ -358,4 +365,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py index 2b3029efde..7a745ae468 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py @@ -10,6 +10,7 @@ from frappe.utils import cstr, getdate, add_days from frappe import _ from frappe.model.mapper import get_mapped_doc + class PatientEncounter(Document): def validate(self): self.set_title() @@ -33,6 +34,85 @@ class PatientEncounter(Document): self.title = _('{0} with {1}').format(self.patient_name or self.patient, self.practitioner_name or self.practitioner)[:100] + @frappe.whitelist() + @staticmethod + def get_applicable_treatment_plans(encounter): + patient = frappe.get_doc('Patient', encounter['patient']) + + plan_filters = {} + plan_filters['name'] = ['in', []] + + age = patient.age + if age: + plan_filters['patient_age_from'] = ['<=', age.years] + plan_filters['patient_age_to'] = ['>=', age.years] + + gender = patient.sex + if gender: + plan_filters['gender'] = ['in', [gender, None]] + + diagnosis = encounter.get('diagnosis') + if diagnosis: + diagnosis = [_diagnosis['diagnosis'] for _diagnosis in encounter['diagnosis']] + filters = [ + ['diagnosis', 'in', diagnosis], + ['parenttype', '=', 'Treatment Plan Template'], + ] + diagnosis = frappe.get_list('Patient Encounter Diagnosis', filters=filters, fields='*') + plan_names = [_diagnosis['parent'] for _diagnosis in diagnosis] + plan_filters['name'][1].extend(plan_names) + + symptoms = encounter.get('symptoms') + if symptoms: + symptoms = [symptom['complaint'] for symptom in encounter['symptoms']] + filters = [ + ['complaint', 'in', symptoms], + ['parenttype', '=', 'Treatment Plan Template'], + ] + symptoms = frappe.get_list('Patient Encounter Symptom', filters=filters, fields='*') + plan_names = [symptom['parent'] for symptom in symptoms] + plan_filters['name'][1].extend(plan_names) + + if not plan_filters['name'][1]: + plan_filters.pop('name') + + plans = frappe.get_list('Treatment Plan Template', fields='*', filters=plan_filters) + + return plans + + @frappe.whitelist() + def set_treatment_plans(self, treatment_plans=None): + for treatment_plan in treatment_plans: + self.set_treatment_plan(treatment_plan) + + def set_treatment_plan(self, plan): + plan_items = frappe.get_list('Treatment Plan Template Item', filters={'parent': plan}, fields='*') + for plan_item in plan_items: + self.set_treatment_plan_item(plan_item) + + drugs = frappe.get_list('Drug Prescription', filters={'parent': plan}, fields='*') + for drug in drugs: + self.append('drug_prescription', drug) + + self.save() + + def set_treatment_plan_item(self, plan_item): + if plan_item.type == 'Clinical Procedure Template': + self.append('procedure_prescription', { + 'procedure': plan_item.template + }) + + if plan_item.type == 'Lab Test Template': + self.append('lab_test_prescription', { + 'lab_test_code': plan_item.template + }) + + if plan_item.type == 'Therapy Type': + self.append('therapies', { + 'therapy_type': plan_item.template + }) + + @frappe.whitelist() def make_ip_medication_order(source_name, target_doc=None): def set_missing_values(source, target): diff --git a/erpnext/healthcare/doctype/patient_encounter/test_patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/test_patient_encounter.js deleted file mode 100644 index 1baabf7eef..0000000000 --- a/erpnext/healthcare/doctype/patient_encounter/test_patient_encounter.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Patient Encounter", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Patient Encounter - () => frappe.tests.make('Patient Encounter', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/patient_encounter/test_patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/test_patient_encounter.py index f5df152050..96976821a7 100644 --- a/erpnext/healthcare/doctype/patient_encounter/test_patient_encounter.py +++ b/erpnext/healthcare/doctype/patient_encounter/test_patient_encounter.py @@ -4,5 +4,82 @@ from __future__ import unicode_literals import unittest +import frappe +from erpnext.healthcare.doctype.patient_encounter.patient_encounter import PatientEncounter + + class TestPatientEncounter(unittest.TestCase): - pass + def setUp(self): + try: + gender_m = frappe.get_doc({ + 'doctype': 'Gender', + 'gender': 'MALE' + }).insert() + gender_f = frappe.get_doc({ + 'doctype': 'Gender', + 'gender': 'FEMALE' + }).insert() + except frappe.exceptions.DuplicateEntryError: + gender_m = frappe.get_doc({ + 'doctype': 'Gender', + 'gender': 'MALE' + }) + gender_f = frappe.get_doc({ + 'doctype': 'Gender', + 'gender': 'FEMALE' + }) + + self.patient_male = frappe.get_doc({ + 'doctype': 'Patient', + 'first_name': 'John', + 'sex': gender_m.gender, + }).insert() + self.patient_female = frappe.get_doc({ + 'doctype': 'Patient', + 'first_name': 'Curie', + 'sex': gender_f.gender, + }).insert() + self.practitioner = frappe.get_doc({ + 'doctype': 'Healthcare Practitioner', + 'first_name': 'Doc', + 'sex': 'MALE', + }).insert() + try: + self.care_plan_male = frappe.get_doc({ + 'doctype': 'Treatment Plan Template', + 'template_name': 'test plan - m', + 'gender': gender_m.gender, + }).insert() + self.care_plan_female = frappe.get_doc({ + 'doctype': 'Treatment Plan Template', + 'template_name': 'test plan - f', + 'gender': gender_f.gender, + }).insert() + except frappe.exceptions.DuplicateEntryError: + self.care_plan_male = frappe.get_doc({ + 'doctype': 'Treatment Plan Template', + 'template_name': 'test plan - m', + 'gender': gender_m.gender, + }) + self.care_plan_female = frappe.get_doc({ + 'doctype': 'Treatment Plan Template', + 'template_name': 'test plan - f', + 'gender': gender_f.gender, + }) + + def test_treatment_plan_template_filter(self): + encounter = frappe.get_doc({ + 'doctype': 'Patient Encounter', + 'patient': self.patient_male.name, + 'practitioner': self.practitioner.name, + }).insert() + plans = PatientEncounter.get_applicable_treatment_plans(encounter.as_dict()) + self.assertEqual(plans[0]['name'], self.care_plan_male.template_name) + + encounter = frappe.get_doc({ + 'doctype': 'Patient Encounter', + 'patient': self.patient_female.name, + 'practitioner': self.practitioner.name, + }).insert() + plans = PatientEncounter.get_applicable_treatment_plans(encounter.as_dict()) + self.assertEqual(plans[0]['name'], self.care_plan_female.template_name) diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 63b00859d7..9e0d3c3e27 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -18,7 +18,7 @@ class PatientHistorySettings(Document): def validate_submittable_doctypes(self): for entry in self.custom_doctypes: if not cint(frappe.db.get_value('DocType', entry.document_type, 'is_submittable')): - msg = _('Row #{0}: Document Type {1} is not submittable. ').format( + msg = _('Row #{0}: Document Type {1} is not submittable.').format( entry.idx, frappe.bold(entry.document_type)) msg += _('Patient Medical Record can only be created for submittable document types.') frappe.throw(msg) @@ -116,12 +116,12 @@ def set_subject_field(doc): 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) + '
' + 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) + '
' + subject += frappe.bold(_(entry.get('label')) + ':') + cstr(formatted_value) + '
' return subject diff --git a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py index 33119d8185..9169ea642b 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py @@ -38,13 +38,12 @@ class TestPatientHistorySettings(unittest.TestCase): # tests for medical record creation of standard doctypes in test_patient_medical_record.py patient = create_patient() doc = create_doc(patient) - # check for medical record medical_rec = frappe.db.exists("Patient Medical Record", {"status": "Open", "reference_name": doc.name}) self.assertTrue(medical_rec) medical_rec = frappe.get_doc("Patient Medical Record", medical_rec) - expected_subject = "Date: {0}Rating: 3Feedback: Test Patient History Settings".format( + expected_subject = "Date:{0}Rating:3Feedback:Test Patient History Settings".format( frappe.utils.format_date(getdate())) self.assertEqual(strip_html(medical_rec.subject), expected_subject) self.assertEqual(medical_rec.patient, patient) diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.js b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.js deleted file mode 100644 index 66dda09e25..0000000000 --- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Patient Medical Record", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Patient Medical Record - () => frappe.tests.make('Patient Medical Record', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py index f8ccc8a002..5b7d8d62c8 100644 --- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py +++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import unittest import frappe from frappe.utils import nowdate -from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment +from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment, create_medical_department from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile class TestPatientMedicalRecord(unittest.TestCase): @@ -15,7 +15,8 @@ class TestPatientMedicalRecord(unittest.TestCase): make_pos_profile() def test_medical_record(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() + medical_department = create_medical_department() appointment = create_appointment(patient, practitioner, nowdate(), invoice=1) encounter = create_encounter(appointment) diff --git a/erpnext/healthcare/doctype/practitioner_schedule/test_practitioner_schedule.js b/erpnext/healthcare/doctype/practitioner_schedule/test_practitioner_schedule.js deleted file mode 100644 index 32dac2c652..0000000000 --- a/erpnext/healthcare/doctype/practitioner_schedule/test_practitioner_schedule.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Practitioner Schedule", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Practitioner Schedule - () => frappe.tests.make('Practitioner Schedule', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/prescription_dosage/test_prescription_dosage.js b/erpnext/healthcare/doctype/prescription_dosage/test_prescription_dosage.js deleted file mode 100644 index 009614ff5d..0000000000 --- a/erpnext/healthcare/doctype/prescription_dosage/test_prescription_dosage.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Prescription Dosage", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Prescription Dosage - () => frappe.tests.make('Prescription Dosage', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/prescription_duration/test_prescription_duration.js b/erpnext/healthcare/doctype/prescription_duration/test_prescription_duration.js deleted file mode 100644 index 4971e79198..0000000000 --- a/erpnext/healthcare/doctype/prescription_duration/test_prescription_duration.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Prescription Duration", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Prescription Duration - () => frappe.tests.make('Prescription Duration', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/sample_collection/test_sample_collection.js b/erpnext/healthcare/doctype/sample_collection/test_sample_collection.js deleted file mode 100644 index 2b4aed756b..0000000000 --- a/erpnext/healthcare/doctype/sample_collection/test_sample_collection.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Sample Collection", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Sample Collection - () => frappe.tests.make('Sample Collection', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/sensitivity/test_sensitivity.js b/erpnext/healthcare/doctype/sensitivity/test_sensitivity.js deleted file mode 100644 index c2cf406f96..0000000000 --- a/erpnext/healthcare/doctype/sensitivity/test_sensitivity.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Sensitivity", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Sensitivity - () => frappe.tests.make('Sensitivity', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py index 113fa513f9..983fba9f5f 100644 --- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py @@ -8,11 +8,13 @@ import unittest from frappe.utils import getdate, flt, nowdate from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice -from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient, create_appointment +from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import \ + create_healthcare_docs, create_patient, create_appointment, create_medical_department class TestTherapyPlan(unittest.TestCase): def test_creation_on_encounter_submission(self): - patient, medical_department, practitioner = create_healthcare_docs() + patient, practitioner = create_healthcare_docs() + medical_department = create_medical_department() encounter = create_encounter(patient, medical_department, practitioner) self.assertTrue(frappe.db.exists('Therapy Plan', encounter.therapy_plan)) @@ -28,8 +30,9 @@ class TestTherapyPlan(unittest.TestCase): frappe.get_doc(session).submit() self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed') - patient, medical_department, practitioner = create_healthcare_docs() - appointment = create_appointment(patient, practitioner, nowdate()) + patient, practitioner = create_healthcare_docs() + appointment = create_appointment(patient, practitioner, nowdate()) + session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name) session = frappe.get_doc(session) session.submit() diff --git a/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py index a5dad293e3..80fc83fd6c 100644 --- a/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py +++ b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py @@ -34,7 +34,8 @@ def create_therapy_type(): }) therapy_type.save() else: - therapy_type = frappe.get_doc('Therapy Type', 'Basic Rehab') + therapy_type = frappe.get_doc('Therapy Type', therapy_type) + return therapy_type def create_exercise_type(): @@ -47,4 +48,7 @@ def create_exercise_type(): 'description': 'Squat and Rise' }) exercise_type.save() + else: + exercise_type = frappe.get_doc('Exercise Type', exercise_type) + return exercise_type diff --git a/erpnext/healthcare/doctype/treatment_plan_template/__init__.py b/erpnext/healthcare/doctype/treatment_plan_template/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/treatment_plan_template/test_records.json b/erpnext/healthcare/doctype/treatment_plan_template/test_records.json new file mode 100644 index 0000000000..d661b4304f --- /dev/null +++ b/erpnext/healthcare/doctype/treatment_plan_template/test_records.json @@ -0,0 +1,7 @@ +[ + { + "doctype": "Treatment Plan Template", + "template_name": "Chemo", + "patient_age_from": 21 + } +] diff --git a/erpnext/healthcare/doctype/treatment_plan_template/test_treatment_plan_template.py b/erpnext/healthcare/doctype/treatment_plan_template/test_treatment_plan_template.py new file mode 100644 index 0000000000..21ede7129f --- /dev/null +++ b/erpnext/healthcare/doctype/treatment_plan_template/test_treatment_plan_template.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestTreatmentPlanTemplate(unittest.TestCase): + pass diff --git a/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.js b/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.js new file mode 100644 index 0000000000..986c3cb6e4 --- /dev/null +++ b/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.js @@ -0,0 +1,14 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Treatment Plan Template', { + refresh: function (frm) { + frm.set_query('type', 'items', function () { + return { + filters: { + 'name': ['in', ['Lab Test Template', 'Clinical Procedure Template', 'Therapy Type']], + } + }; + }); + }, +}); diff --git a/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.json b/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.json new file mode 100644 index 0000000000..85a312fb17 --- /dev/null +++ b/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.json @@ -0,0 +1,189 @@ +{ + "actions": [], + "autoname": "field:template_name", + "creation": "2021-06-10 10:14:17.901273", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_1", + "template_name", + "description", + "practitioners", + "disabled", + "column_break_1", + "medical_department", + "goal", + "order_group", + "section_break_8", + "patient_age_from", + "complaints", + "gender", + "column_break_12", + "patient_age_to", + "diagnosis", + "plan_items_section", + "items", + "drugs" + ], + "fields": [ + { + "fieldname": "section_break_1", + "fieldtype": "Section Break", + "label": "Plan Details" + }, + { + "fieldname": "medical_department", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Medical Department", + "options": "Medical Department" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "goal", + "fieldtype": "Small Text", + "label": "Goal" + }, + { + "fieldname": "practitioners", + "fieldtype": "Table MultiSelect", + "label": "Practitioners", + "options": "Treatment Plan Template Practitioner" + }, + { + "fieldname": "order_group", + "fieldtype": "Link", + "label": "Order Group", + "options": "Patient Encounter", + "read_only": 1 + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "label": "Plan Conditions" + }, + { + "fieldname": "template_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Template Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "patient_age_from", + "fieldtype": "Int", + "label": "Patient Age From", + "non_negative": 1 + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "fieldname": "patient_age_to", + "fieldtype": "Int", + "label": "Patient Age To", + "non_negative": 1 + }, + { + "fieldname": "gender", + "fieldtype": "Link", + "label": "Gender", + "options": "Gender" + }, + { + "fieldname": "complaints", + "fieldtype": "Table MultiSelect", + "label": "Complaints", + "options": "Patient Encounter Symptom" + }, + { + "fieldname": "diagnosis", + "fieldtype": "Table MultiSelect", + "label": "Diagnosis", + "options": "Patient Encounter Diagnosis" + }, + { + "fieldname": "plan_items_section", + "fieldtype": "Section Break", + "label": "Plan Items" + }, + { + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Treatment Plan Template Item" + }, + { + "fieldname": "drugs", + "fieldtype": "Table", + "label": "Drugs", + "options": "Drug Prescription" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-08-18 02:41:58.354296", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Treatment Plan Template", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Physician", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Healthcare Administrator", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "template_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.py b/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.py new file mode 100644 index 0000000000..a92e2668fe --- /dev/null +++ b/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template.py @@ -0,0 +1,19 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + +class TreatmentPlanTemplate(Document): + def validate(self): + self.validate_age() + + def validate_age(self): + if self.patient_age_from and self.patient_age_from < 0: + frappe.throw(_('Patient Age From cannot be less than 0')) + if self.patient_age_to and self.patient_age_to < 0: + frappe.throw(_('Patient Age To cannot be less than 0')) + if self.patient_age_to and self.patient_age_from and \ + self.patient_age_to < self.patient_age_from: + frappe.throw(_('Patient Age To cannot be less than Patient Age From')) diff --git a/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template_list.js b/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template_list.js new file mode 100644 index 0000000000..7ab31dff79 --- /dev/null +++ b/erpnext/healthcare/doctype/treatment_plan_template/treatment_plan_template_list.js @@ -0,0 +1,10 @@ +frappe.listview_settings['Treatment Plan Template'] = { + get_indicator: function(doc) { + var colors = { + 1: 'gray', + 0: 'blue', + }; + let label = doc.disabled == 1 ? 'Disabled' : 'Enabled'; + return [__(label), colors[doc.disabled], 'disable,=,' + doc.disabled]; + } +}; diff --git a/erpnext/healthcare/doctype/treatment_plan_template_item/__init__.py b/erpnext/healthcare/doctype/treatment_plan_template_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/treatment_plan_template_item/treatment_plan_template_item.json b/erpnext/healthcare/doctype/treatment_plan_template_item/treatment_plan_template_item.json new file mode 100644 index 0000000000..20a9d6793a --- /dev/null +++ b/erpnext/healthcare/doctype/treatment_plan_template_item/treatment_plan_template_item.json @@ -0,0 +1,55 @@ +{ + "actions": [], + "creation": "2021-06-10 11:47:29.194795", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "type", + "template", + "qty", + "instructions" + ], + "fields": [ + { + "fieldname": "type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "template", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Template", + "options": "type", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "qty", + "fieldtype": "Int", + "label": "Qty" + }, + { + "fieldname": "instructions", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Instructions" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-08-17 11:19:03.515441", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Treatment Plan Template Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/treatment_plan_template_item/treatment_plan_template_item.py b/erpnext/healthcare/doctype/treatment_plan_template_item/treatment_plan_template_item.py new file mode 100644 index 0000000000..5f58b06af6 --- /dev/null +++ b/erpnext/healthcare/doctype/treatment_plan_template_item/treatment_plan_template_item.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class TreatmentPlanTemplateItem(Document): + pass diff --git a/erpnext/healthcare/doctype/treatment_plan_template_practitioner/__init__.py b/erpnext/healthcare/doctype/treatment_plan_template_practitioner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/treatment_plan_template_practitioner/treatment_plan_template_practitioner.json b/erpnext/healthcare/doctype/treatment_plan_template_practitioner/treatment_plan_template_practitioner.json new file mode 100644 index 0000000000..04da387f7b --- /dev/null +++ b/erpnext/healthcare/doctype/treatment_plan_template_practitioner/treatment_plan_template_practitioner.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "creation": "2021-06-10 10:37:56.669416", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "practitioner" + ], + "fields": [ + { + "fieldname": "practitioner", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Practitioner", + "options": "Healthcare Practitioner", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-06-11 16:05:06.733299", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Treatment Plan Template Practitioner", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/treatment_plan_template_practitioner/treatment_plan_template_practitioner.py b/erpnext/healthcare/doctype/treatment_plan_template_practitioner/treatment_plan_template_practitioner.py new file mode 100644 index 0000000000..6d34568e15 --- /dev/null +++ b/erpnext/healthcare/doctype/treatment_plan_template_practitioner/treatment_plan_template_practitioner.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class TreatmentPlanTemplatePractitioner(Document): + pass diff --git a/erpnext/healthcare/doctype/vital_signs/test_vital_signs.js b/erpnext/healthcare/doctype/vital_signs/test_vital_signs.js deleted file mode 100644 index f4ab4466be..0000000000 --- a/erpnext/healthcare/doctype/vital_signs/test_vital_signs.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Vital Signs", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Vital Signs - () => frappe.tests.make('Vital Signs', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/healthcare/module_onboarding/healthcare/healthcare.json b/erpnext/healthcare/module_onboarding/healthcare/healthcare.json index 56c3c13559..0aa8f9a027 100644 --- a/erpnext/healthcare/module_onboarding/healthcare/healthcare.json +++ b/erpnext/healthcare/module_onboarding/healthcare/healthcare.json @@ -10,7 +10,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/healthcare", "idx": 0, "is_complete": 0, - "modified": "2020-07-08 14:06:19.512946", + "modified": "2021-01-30 19:22:20.273766", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare", diff --git a/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json b/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json index c45a347080..3f25a9d676 100644 --- a/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json +++ b/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-26 23:16:31.965521", + "modified": "2021-01-30 12:02:22.849260", "modified_by": "Administrator", "name": "Create Healthcare Practitioner", "owner": "Administrator", "reference_document": "Healthcare Practitioner", + "show_form_tour": 0, "show_full_form": 1, "title": "Create Healthcare Practitioner", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/create_patient/create_patient.json b/erpnext/healthcare/onboarding_step/create_patient/create_patient.json index 77bc5bd7ad..b46bb15b48 100644 --- a/erpnext/healthcare/onboarding_step/create_patient/create_patient.json +++ b/erpnext/healthcare/onboarding_step/create_patient/create_patient.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-19 12:26:24.023418", - "modified_by": "Administrator", + "modified": "2021-01-30 00:09:28.786428", + "modified_by": "ruchamahabal2@gmail.com", "name": "Create Patient", "owner": "Administrator", "reference_document": "Patient", + "show_form_tour": 0, "show_full_form": 1, "title": "Create Patient", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json b/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json index 65980ef668..7ce122d5c0 100644 --- a/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json +++ b/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-19 12:27:09.437825", - "modified_by": "Administrator", + "modified": "2021-01-30 00:09:28.794602", + "modified_by": "ruchamahabal2@gmail.com", "name": "Create Practitioner Schedule", "owner": "Administrator", "reference_document": "Practitioner Schedule", + "show_form_tour": 0, "show_full_form": 1, "title": "Create Practitioner Schedule", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json b/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json index 697b761e52..dfe9f71a76 100644 --- a/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json +++ b/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-26 23:10:24.504030", + "modified": "2021-01-30 19:22:08.257160", "modified_by": "Administrator", "name": "Explore Clinical Procedure Templates", "owner": "Administrator", "reference_document": "Clinical Procedure Template", + "show_form_tour": 0, "show_full_form": 0, "title": "Explore Clinical Procedure Templates", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json b/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json index b2d5aef431..2d952f3093 100644 --- a/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json +++ b/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 1, "is_skipped": 0, - "modified": "2020-05-26 23:10:24.507648", + "modified": "2021-01-30 19:22:07.275735", "modified_by": "Administrator", "name": "Explore Healthcare Settings", "owner": "Administrator", "reference_document": "Healthcare Settings", + "show_form_tour": 0, "show_full_form": 0, "title": "Explore Healthcare Settings", "validate_action": 1 diff --git a/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json b/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json index fa4c9036d7..baa8358c06 100644 --- a/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json +++ b/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json @@ -6,14 +6,14 @@ "field": "schedule", "idx": 0, "is_complete": 0, - "is_mandatory": 1, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-26 22:07:07.482530", - "modified_by": "Administrator", + "modified": "2021-01-30 00:09:28.807129", + "modified_by": "ruchamahabal2@gmail.com", "name": "Introduction to Healthcare Practitioner", "owner": "Administrator", "reference_document": "Healthcare Practitioner", + "show_form_tour": 0, "show_full_form": 0, "title": "Introduction to Healthcare Practitioner", "validate_action": 0 diff --git a/erpnext/healthcare/page/patient_history/patient_history.css b/erpnext/healthcare/page/patient_history/patient_history.css index 1bb589164e..74b5e7eb91 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.css +++ b/erpnext/healthcare/page/patient_history/patient_history.css @@ -9,6 +9,26 @@ cursor: pointer; } +.patient-image-container { + margin-top: 17px; + } + +.patient-image { + display: inline-block; + width: 100%; + height: 0; + padding: 50% 0px; + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + border-radius: 4px; +} + +.patient-name { + font-size: 20px; + margin-top: 25px; +} + .medical_record-label { max-width: 100px; margin-bottom: -4px; @@ -19,19 +39,19 @@ } .date-indicator { - background:none; - font-size:12px; - vertical-align:middle; - font-weight:bold; - color:#6c7680; + background:none; + font-size:12px; + vertical-align:middle; + font-weight:bold; + color:#6c7680; } .date-indicator::after { - margin:0 -4px 0 12px; - content:''; - display:inline-block; - height:8px; - width:8px; - border-radius:8px; + margin:0 -4px 0 12px; + content:''; + display:inline-block; + height:8px; + width:8px; + border-radius:8px; background: #d1d8dd; } diff --git a/erpnext/healthcare/page/patient_history/patient_history.html b/erpnext/healthcare/page/patient_history/patient_history.html index f1706557f4..d16b38637c 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.html +++ b/erpnext/healthcare/page/patient_history/patient_history.html @@ -1,26 +1,18 @@ -
-
-

-
+
+
+
+
+
+
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
- -
+ +
+
+
+
+
+
+
+
diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js index 54343aae44..bf947cac21 100644 --- a/erpnext/healthcare/page/patient_history/patient_history.js +++ b/erpnext/healthcare/page/patient_history/patient_history.js @@ -1,403 +1,455 @@ frappe.provide('frappe.patient_history'); frappe.pages['patient_history'].on_page_load = function(wrapper) { - let me = this; - let page = frappe.ui.make_app_page({ + frappe.ui.make_app_page({ parent: wrapper, - title: 'Patient History', - single_column: true + title: __('Patient History') }); - frappe.breadcrumbs.add('Healthcare'); - let pid = ''; - page.main.html(frappe.render_template('patient_history', {})); - page.main.find('.header-separator').hide(); - - let patient = frappe.ui.form.make_control({ - parent: page.main.find('.patient'), - df: { - fieldtype: 'Link', - options: 'Patient', - fieldname: 'patient', - placeholder: __('Select Patient'), - only_select: true, - change: function() { - let patient_id = patient.get_value(); - if (pid != patient_id && patient_id) { - me.start = 0; - me.page.main.find('.patient_documents_list').html(''); - setup_filters(patient_id, me); - get_documents(patient_id, me); - show_patient_info(patient_id, me); - show_patient_vital_charts(patient_id, me, 'bp', 'mmHg', 'Blood Pressure'); - } - pid = patient_id; - } - }, - }); - patient.refresh(); - - if (frappe.route_options) { - patient.set_value(frappe.route_options.patient); - } - - this.page.main.on('click', '.btn-show-chart', function() { - let btn_show_id = $(this).attr('data-show-chart-id'), pts = $(this).attr('data-pts'); - let title = $(this).attr('data-title'); - show_patient_vital_charts(patient.get_value(), me, btn_show_id, pts, title); - }); - - this.page.main.on('click', '.btn-more', function() { - let doctype = $(this).attr('data-doctype'), docname = $(this).attr('data-docname'); - if (me.page.main.find('.'+docname).parent().find('.document-html').attr('data-fetched') == '1') { - me.page.main.find('.'+docname).hide(); - me.page.main.find('.'+docname).parent().find('.document-html').show(); - } else { - if (doctype && docname) { - let exclude = ['patient', 'patient_name', 'patient_sex', 'encounter_date']; - frappe.call({ - method: 'erpnext.healthcare.utils.render_doc_as_html', - args:{ - doctype: doctype, - docname: docname, - exclude_fields: exclude - }, - freeze: true, - callback: function(r) { - if (r.message) { - me.page.main.find('.' + docname).hide(); - - me.page.main.find('.' + docname).parent().find('.document-html').html( - `${r.message.html} -
- - -
- `); - - me.page.main.find('.' + docname).parent().find('.document-html').show(); - me.page.main.find('.' + docname).parent().find('.document-html').attr('data-fetched', '1'); - } - } - }); - } - } - }); - - this.page.main.on('click', '.btn-less', function() { - let docname = $(this).attr('data-docname'); - me.page.main.find('.' + docname).parent().find('.document-id').show(); - me.page.main.find('.' + docname).parent().find('.document-html').hide(); - }); - me.start = 0; - me.page.main.on('click', '.btn-get-records', function() { - get_documents(patient.get_value(), me); + let patient_history = new PatientHistory(wrapper); + $(wrapper).bind('show', ()=> { + patient_history.show(); }); }; -let setup_filters = function(patient, me) { - $('.doctype-filter').empty(); - frappe.xcall( - 'erpnext.healthcare.page.patient_history.patient_history.get_patient_history_doctypes' - ).then(document_types => { - let doctype_filter = frappe.ui.form.make_control({ - parent: $('.doctype-filter'), +class PatientHistory { + constructor(wrapper) { + this.wrapper = $(wrapper); + this.page = wrapper.page; + this.sidebar = this.wrapper.find('.layout-side-section'); + this.main_section = this.wrapper.find('.layout-main-section'); + this.start = 0; + } + + show() { + frappe.breadcrumbs.add('Healthcare'); + this.sidebar.empty(); + + let me = this; + let patient = frappe.ui.form.make_control({ + parent: me.sidebar, df: { - fieldtype: 'MultiSelectList', - fieldname: 'document_type', - placeholder: __('Select Document Type'), - input_class: 'input-xs', + fieldtype: 'Link', + options: 'Patient', + fieldname: 'patient', + placeholder: __('Select Patient'), + only_select: true, change: () => { - me.start = 0; - me.page.main.find('.patient_documents_list').html(''); - get_documents(patient, me, doctype_filter.get_value(), date_range_field.get_value()); - }, - get_data: () => { - return document_types.map(document_type => { - return { - description: document_type, - value: document_type - }; - }); - }, + me.patient_id = ''; + if (me.patient_id != patient.get_value() && patient.get_value()) { + me.start = 0; + me.patient_id = patient.get_value(); + me.make_patient_profile(); + } + } } }); - doctype_filter.refresh(); + patient.refresh(); - $('.date-filter').empty(); - let date_range_field = frappe.ui.form.make_control({ - df: { - fieldtype: 'DateRange', - fieldname: 'date_range', - placeholder: __('Date Range'), - input_class: 'input-xs', - change: () => { - let selected_date_range = date_range_field.get_value(); - if (selected_date_range && selected_date_range.length === 2) { + if (frappe.route_options && !this.patient_id) { + patient.set_value(frappe.route_options.patient); + this.patient_id = frappe.route_options.patient; + } + + this.sidebar.find('[data-fieldname="patient"]').append('
'); + } + + make_patient_profile() { + this.page.set_title(__('Patient History')); + this.main_section.empty().append(frappe.render_template('patient_history')); + this.setup_filters(); + this.setup_documents(); + this.show_patient_info(); + this.setup_buttons(); + this.show_patient_vital_charts('bp', 'mmHg', 'Blood Pressure'); + } + + setup_filters() { + $('.doctype-filter').empty(); + let me = this; + + frappe.xcall( + 'erpnext.healthcare.page.patient_history.patient_history.get_patient_history_doctypes' + ).then(document_types => { + let doctype_filter = frappe.ui.form.make_control({ + parent: $('.doctype-filter'), + df: { + fieldtype: 'MultiSelectList', + fieldname: 'document_type', + placeholder: __('Select Document Type'), + change: () => { me.start = 0; me.page.main.find('.patient_documents_list').html(''); - get_documents(patient, me, doctype_filter.get_value(), selected_date_range); - } + this.setup_documents(doctype_filter.get_value(), date_range_field.get_value()); + }, + get_data: () => { + return document_types.map(document_type => { + return { + description: document_type, + value: document_type + }; + }); + }, } - }, - parent: $('.date-filter') + }); + doctype_filter.refresh(); + + $('.date-filter').empty(); + let date_range_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'DateRange', + fieldname: 'date_range', + placeholder: __('Date Range'), + input_class: 'input-xs', + change: () => { + let selected_date_range = date_range_field.get_value(); + if (selected_date_range && selected_date_range.length === 2) { + me.start = 0; + me.page.main.find('.patient_documents_list').html(''); + this.setup_documents(doctype_filter.get_value(), date_range_field.get_value()); + } + } + }, + parent: $('.date-filter') + }); + date_range_field.refresh(); }); - date_range_field.refresh(); - }); -}; + } -let get_documents = function(patient, me, document_types="", selected_date_range="") { - let filters = { - name: patient, - start: me.start, - page_length: 20 - }; - if (document_types) - filters['document_types'] = document_types; - if (selected_date_range) - filters['date_range'] = selected_date_range; + setup_documents(document_types="", selected_date_range="") { + let filters = { + name: this.patient_id, + start: this.start, + page_length: 20 + }; + if (document_types) + filters['document_types'] = document_types; + if (selected_date_range) + filters['date_range'] = selected_date_range; - frappe.call({ - 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed', - args: filters, - callback: function(r) { - let data = r.message; - if (data.length) { - add_to_records(me, data); - } else { - me.page.main.find('.patient_documents_list').append(` -
-

${__('No more records..')}

-
`); - me.page.main.find('.btn-get-records').hide(); + let me = this; + frappe.call({ + 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed', + args: filters, + callback: function(r) { + let data = r.message; + if (data.length) { + me.add_to_records(data); + } else { + me.page.main.find('.patient_documents_list').append(` +
+

${__('No more records..')}

+
`); + me.page.main.find('.btn-get-records').hide(); + } } - } - }); -}; + }); + } -let add_to_records = function(me, data) { - let details = "
`; + } + } + + this.page.main.find('.patient_documents_list').append(details); + this.start += data.length; + + if (data.length === 20) { + this.page.main.find(".btn-get-records").show(); + } else { + this.page.main.find(".btn-get-records").hide(); + this.page.main.find(".patient_documents_list").append(` +
+

${__('No more records..')}

+
`); } } - details += ''; - me.page.main.find('.patient_documents_list').append(details); - me.start += data.length; + add_date_separator(data) { + let date = frappe.datetime.str_to_obj(data.communication_date); + let pdate = ''; + let diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), + frappe.datetime.obj_to_str(date)); - if (data.length === 20) { - me.page.main.find(".btn-get-records").show(); - } else { - me.page.main.find(".btn-get-records").hide(); - me.page.main.find(".patient_documents_list").append(` -
-

${__('No more records..')}

-
`); - } -}; - -let add_date_separator = function(data) { - let date = frappe.datetime.str_to_obj(data.communication_date); - let pdate = ''; - let diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date)); - - if (diff < 1) { - pdate = __('Today'); - } else if (diff < 2) { - pdate = __('Yesterday'); - } else { - pdate = __('on ') + frappe.datetime.global_date_format(date); - } - data.date_sep = pdate; - return data; -}; - -let show_patient_info = function(patient, me) { - frappe.call({ - 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail', - args: { - patient: patient - }, - callback: function(r) { - let data = r.message; - let details = ''; - if (data.image) { - details += `
`; - } - - details += ` ${data.patient_name}
${data.sex}`; - if (data.email) details += `
${data.email}`; - if (data.mobile) details += `
${data.mobile}`; - if (data.occupation) details += `

${__('Occupation')} : ${data.occupation}`; - if (data.blood_group) details += `
${__('Blood Group')} : ${data.blood_group}`; - if (data.allergies) details += `

${__('Allerigies')} : ${data.allergies.replace("\n", ", ")}`; - if (data.medication) details += `
${__('Medication')} : ${data.medication.replace("\n", ", ")}`; - if (data.alcohol_current_use) details += `

${__('Alcohol use')} : ${data.alcohol_current_use}`; - if (data.alcohol_past_use) details += `
${__('Alcohol past use')} : ${data.alcohol_past_use}`; - if (data.tobacco_current_use) details += `
${__('Tobacco use')} : ${data.tobacco_current_use}`; - if (data.tobacco_past_use) details += `
${__('Tobacco past use')} : ${data.tobacco_past_use}`; - if (data.medical_history) details += `

${__('Medical history')} : ${data.medical_history.replace("\n", ", ")}`; - if (data.surgical_history) details += `
${__('Surgical history')} : ${data.surgical_history.replace("\n", ", ")}`; - if (data.surrounding_factors) details += `

${__('Occupational hazards')} : ${data.surrounding_factors.replace("\n", ", ")}`; - if (data.other_risk_factors) details += `
${__('Other risk factors')} : ${data.other_risk_factors.replace("\n", ", ")}`; - if (data.patient_details) details += `

${__('More info')} : ${data.patient_details.replace("\n", ", ")}`; - - if (details) { - details = `
` + details + `
`; - } - me.page.main.find('.patient_details').html(details); + if (diff < 1) { + pdate = __('Today'); + } else if (diff < 2) { + pdate = __('Yesterday'); + } else { + pdate = __('on {0}', [frappe.datetime.global_date_format(date)]); } - }); -}; + data.date_sep = pdate; + return data; + } -let show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) { - frappe.call({ - method: 'erpnext.healthcare.utils.get_patient_vitals', - args:{ - patient: patient - }, - callback: function(r) { - if (r.message) { - let show_chart_btns_html = ` - `; + show_patient_info() { + this.get_patient_info().then(() => { + $('.patient-info').empty().append(frappe.render_template('patient_history_sidebar', { + patient_image: this.patient.image, + patient_name: this.patient.patient_name, + patient_gender: this.patient.sex, + patient_mobile: this.patient.mobile + })); + this.show_patient_details(); + }); + } - me.page.main.find('.show_chart_btns').html(show_chart_btns_html); + show_patient_details() { + let me = this; + frappe.call({ + 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail', + args: { + patient: me.patient_id + }, + callback: function(r) { let data = r.message; - let labels = [], datasets = []; - let bp_systolic = [], bp_diastolic = [], temperature = []; - let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = []; + let details = ``; - for (let i=0; i
${__('Occupation')} : ${data.occupation}`; + if (data.blood_group) details += `
${__('Blood Group')} : ${data.blood_group}`; + if (data.allergies) details += `

${__('Allerigies')} : ${data.allergies.replace("\n", ", ")}`; + if (data.medication) details += `
${__('Medication')} : ${data.medication.replace("\n", ", ")}`; + if (data.alcohol_current_use) details += `

${__('Alcohol use')} : ${data.alcohol_current_use}`; + if (data.alcohol_past_use) details += `
${__('Alcohol past use')} : ${data.alcohol_past_use}`; + if (data.tobacco_current_use) details += `
${__('Tobacco use')} : ${data.tobacco_current_use}`; + if (data.tobacco_past_use) details += `
${__('Tobacco past use')} : ${data.tobacco_past_use}`; + if (data.medical_history) details += `

${__('Medical history')} : ${data.medical_history.replace("\n", ", ")}`; + if (data.surgical_history) details += `
${__('Surgical history')} : ${data.surgical_history.replace("\n", ", ")}`; + if (data.surrounding_factors) details += `

${__('Occupational hazards')} : ${data.surrounding_factors.replace("\n", ", ")}`; + if (data.other_risk_factors) details += `
${__('Other risk factors')} : ${data.other_risk_factors.replace("\n", ", ")}`; + if (data.patient_details) details += `

${__('More info')} : ${data.patient_details.replace("\n", ", ")}`; - if (btn_show_id === 'bp') { - bp_systolic.push(data[i].bp_systolic); - bp_diastolic.push(data[i].bp_diastolic); - } - if (btn_show_id === 'temperature') { - temperature.push(data[i].temperature); - } - if (btn_show_id === 'pulse_rate') { - pulse.push(data[i].pulse); - respiratory_rate.push(data[i].respiratory_rate); - } - if (btn_show_id === 'bmi') { - bmi.push(data[i].bmi); - height.push(data[i].height); - weight.push(data[i].weight); - } + if (details) { + details = `
` + details + `
`; } - if (btn_show_id === 'temperature') { - datasets.push({name: 'Temperature', values: temperature, chartType: 'line'}); - } - if (btn_show_id === 'bmi') { - datasets.push({name: 'BMI', values: bmi, chartType: 'line'}); - datasets.push({name: 'Height', values: height, chartType: 'line'}); - datasets.push({name: 'Weight', values: weight, chartType: 'line'}); - } - if (btn_show_id === 'bp') { - datasets.push({name: 'BP Systolic', values: bp_systolic, chartType: 'line'}); - datasets.push({name: 'BP Diastolic', values: bp_diastolic, chartType: 'line'}); - } - if (btn_show_id === 'pulse_rate') { - datasets.push({name: 'Heart Rate / Pulse', values: pulse, chartType: 'line'}); - datasets.push({name: 'Respiratory Rate', values: respiratory_rate, chartType: 'line'}); - } - new frappe.Chart('.patient_vital_charts', { - data: { - labels: labels, - datasets: datasets - }, - title: title, - type: 'axis-mixed', - height: 200, - colors: ['purple', '#ffa3ef', 'light-blue'], - - tooltipOptions: { - formatTooltipX: d => (d + '').toUpperCase(), - formatTooltipY: d => d + ' ' + pts, - } - }); - me.page.main.find('.header-separator').show(); - } else { - me.page.main.find('.patient_vital_charts').html(''); - me.page.main.find('.show_chart_btns').html(''); - me.page.main.find('.header-separator').hide(); + me.sidebar.find('.patient-details').html(details); } - } - }); -}; + }); + } + + get_patient_info() { + return frappe.xcall('frappe.client.get', { + doctype: 'Patient', + name: this.patient_id, + }).then((patient) => { + if (patient) { + this.patient = patient; + } + }); + } + + setup_buttons() { + let me = this; + this.page.main.on("click", ".btn-show-chart", function() { + let btn_id = $(this).attr("data-show-chart-id"), scale_unit = $(this).attr("data-pts"); + let title = $(this).attr("data-title"); + me.show_patient_vital_charts(btn_id, scale_unit, title); + }); + + this.page.main.on('click', '.btn-more', function() { + let doctype = $(this).attr('data-doctype'), docname = $(this).attr('data-docname'); + if (me.page.main.find('.'+docname).parent().find('.document-html').attr('data-fetched') == '1') { + me.page.main.find('.'+docname).hide(); + me.page.main.find('.'+docname).parent().find('.document-html').show(); + } else { + if (doctype && docname) { + let exclude = ['patient', 'patient_name', 'patient_sex', 'encounter_date', 'naming_series']; + frappe.call({ + method: 'erpnext.healthcare.utils.render_doc_as_html', + args: { + doctype: doctype, + docname: docname, + exclude_fields: exclude + }, + freeze: true, + callback: function(r) { + if (r.message) { + me.page.main.find('.' + docname).hide(); + + me.page.main.find('.' + docname).parent().find('.document-html').html( + `${r.message.html} +
+
+ + +
+ `); + + me.page.main.find('.' + docname).parent().find('.document-html').attr('hidden', false); + me.page.main.find('.' + docname).parent().find('.document-html').attr('data-fetched', '1'); + } + } + }); + } + } + }); + + this.page.main.on('click', '.btn-less', function() { + let docname = $(this).attr('data-docname'); + me.page.main.find('.' + docname).parent().find('.document-id').show(); + me.page.main.find('.' + docname).parent().find('.document-html').hide(); + }); + + me.page.main.on('click', '.btn-get-records', function() { + this.setup_documents(); + }); + } + + show_patient_vital_charts(btn_id, scale_unit, title) { + let me = this; + + frappe.call({ + method: 'erpnext.healthcare.utils.get_patient_vitals', + args: { + patient: me.patient_id + }, + callback: function(r) { + if (r.message) { + let show_chart_btns_html = ` + `; + + me.page.main.find('.show_chart_btns').html(show_chart_btns_html); + let data = r.message; + let labels = [], datasets = []; + let bp_systolic = [], bp_diastolic = [], temperature = []; + let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = []; + + for (let i=0; i (d + '').toUpperCase(), + formatTooltipY: d => d + ' ' + scale_unit, + } + }); + me.page.main.find('.header-separator').show(); + } else { + me.page.main.find('.patient_vital_charts').html(''); + me.page.main.find('.show_chart_btns').html(''); + me.page.main.find('.header-separator').hide(); + } + } + }); + } +} \ No newline at end of file diff --git a/erpnext/healthcare/page/patient_history/patient_history_sidebar.html b/erpnext/healthcare/page/patient_history/patient_history_sidebar.html new file mode 100644 index 0000000000..4560e7e125 --- /dev/null +++ b/erpnext/healthcare/page/patient_history/patient_history_sidebar.html @@ -0,0 +1,21 @@ +
+
+ {% if patient_image %} +
+ {% endif %} +
+
+ {% if patient_name %} +

{{patient_name}}

+ {% endif %} + {% if patient_gender %} +

{%=__("Gender: ") %} {{patient_gender}}

+ {% endif %} + {% if patient_mobile %} +

{%=__("Contact: ") %} {{patient_mobile}}

+ {% endif %} +
+
+
+
+ diff --git a/erpnext/healthcare/page/patient_progress/patient_progress.css b/erpnext/healthcare/page/patient_progress/patient_progress.css index 5d85a7487f..737b2e0ea2 100644 --- a/erpnext/healthcare/page/patient_progress/patient_progress.css +++ b/erpnext/healthcare/page/patient_progress/patient_progress.css @@ -29,6 +29,7 @@ .patient-name { font-size: 20px; + margin-top: 25px; } /* heatmap */ @@ -55,6 +56,7 @@ } .heatmap-container .chart-filter { + z-index: 1; position: relative; top: 5px; margin-right: 10px; @@ -111,10 +113,13 @@ text.title { } .chart-column-container { - border-bottom: 1px solid #d1d8dd; margin: 5px 0; } +.progress-graphs .progress-container { + margin-bottom: var(--margin-xl); +} + .line-chart-container .frappe-chart { margin-top: -20px; } @@ -146,6 +151,7 @@ text.title { } .percentage-chart-container .chart-filter { + z-index: 1; position: relative; top: 12px; margin-right: 10px; diff --git a/erpnext/healthcare/page/patient_progress/patient_progress.html b/erpnext/healthcare/page/patient_progress/patient_progress.html index 30064bd165..ee60065618 100644 --- a/erpnext/healthcare/page/patient_progress/patient_progress.html +++ b/erpnext/healthcare/page/patient_progress/patient_progress.html @@ -1,14 +1,15 @@
-